Skip to content

✨ [RUM-16985] Capture wasm module build_ids and enrich error events - #4920

Open
ImaneLargou wants to merge 18 commits into
mainfrom
imane.largou/wasm-sdk-experiment
Open

✨ [RUM-16985] Capture wasm module build_ids and enrich error events#4920
ImaneLargou wants to merge 18 commits into
mainfrom
imane.largou/wasm-sdk-experiment

Conversation

@ImaneLargou

@ImaneLargou ImaneLargou commented Jul 27, 2026

Copy link
Copy Markdown

Motivation

Browser WebAssembly runtime errors currently lack the module metadata required for WASM symbolication.

This change identifies errors containing WebAssembly stack frames and reports the loaded WASM module URL and build ID. This allows the error-processing pipeline to distinguish ordinary browser errors from errors that should use WASM/DWARF symbolication.

Related upstream schema change: DataDog/rum-events-format#427

Changes

  • Track WebAssembly modules loaded through:
    • WebAssembly.instantiate
    • WebAssembly.instantiateStreaming
    • WebAssembly.compile
    • WebAssembly.compileStreaming
  • Extract module build IDs from the build_id custom section, with external_debug_info as a fallback.
  • Detect WASM errors by checking the complete error stack for WASM-specific frames.
  • Report the following metadata on WASM errors:
    • error.source_type: "browser+wasm"
    • error.wasm_modules, containing the module URL and build ID
  • Add support to both RUM and Browser Logs.
  • Keep ordinary JavaScript errors unchanged. In particular, .js frames do not qualify an error as WASM.
  • Update the generated RUM event types from the corresponding rum-events-format schema change.

Test instructions

Run the relevant unit tests:

  • packages/browser-core/src/domain/wasmModules/wasmBinaryParser.spec.ts
  • packages/browser-core/src/domain/wasmModules/wasmModuleTracking.spec.ts
  • packages/browser-rum-core/src/domain/error/errorCollection.spec.ts
  • packages/browser-logs/src/domain/createErrorFieldFromRawError.spec.ts

Run the E2E scenario:

yarn test:e2e -g "send WebAssembly runtime errors with module metadata"

The E2E test loads a real .wasm module using WebAssembly.instantiateStreaming, triggers a runtime error, and verifies that both RUM and Logs events contain:

  {
    "source_type": "browser+wasm",
    "wasm_modules": [
      {
        "url": "<test module URL>",
        "build_id": "abcd"
      }
    ]
  }

Checklist

  • Tested locally
  • Tested on staging
  • Added unit tests for this change.
  • Added e2e/integration tests for this change.
  • Updated documentation and/or relevant AGENTS.md file

Intercepts WebAssembly.instantiate / instantiateStreaming at SDK
script-load time to record (url, build_id) per loaded module, including
lazily-loaded modules. On error capture, attaches error.wasm_modules[]
and sets source_type='browser+wasm' so the backend can dispatch wasm
symbolication.

- New wasmModules/wasmModuleTracking.ts: hooks all four WebAssembly
  entry points; reads build_id via a minimal custom-section parser;
  registry stays live for lazy module loads
- New wasmModules/wasmBinaryParser.ts: walks wasm binary sections,
  extracts build_id custom section or falls back to external_debug_info
- errorCollection: populates error.wasm_modules[] and flips source_type
  to 'browser+wasm' when any module is registered
- rawRumEvent.types: adds wasm_modules?: Array<{url, build_id}> to
  RawRumErrorEvent
- main.ts: installs tracking synchronously at script-load time to close
  the race window before DD_RUM.init()'s deferred microtask

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@datadog-official

datadog-official Bot commented Jul 27, 2026

Copy link
Copy Markdown

Pipelines  Tests

Unblock PR with BitsAI

⚠️ Warnings

Your PR has failed checks. Please review the issues below and take necessary action before merging.

🚦 2 Pipeline jobs failed

DataDog/browser-sdk | build-and-lint — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

DataDog/browser-sdk | typecheck — 🔧 Needs a code fix, caused by this PR

View more details · View in GitLab

ℹ️ Info

No other issues found (see more)

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 80.77%
Overall Coverage: 77.03% (+0.02%)

Useful? React with 👍 / 👎

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 58d9927 | Docs | View more details | Give us feedback!

@ImaneLargou ImaneLargou changed the title ✨ [RUM-WASM] Capture wasm module build_ids and enrich error events [WASM] Capture wasm module build_ids and enrich error events Jul 27, 2026
@ImaneLargou
ImaneLargou marked this pull request as ready for review August 11, 2026 09:47
@ImaneLargou
ImaneLargou requested a review from a team as a code owner August 11, 2026 09:47
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@cit-pr-commenter-54b7da

cit-pr-commenter-54b7da Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bundles Sizes Evolution

📦 Bundle Name Base Size Local Size 𝚫 𝚫% Status
Rum 182.00 KiB 182.06 KiB +69 B +0.04%
Rum Profiler 8.43 KiB 8.43 KiB 0 B 0.00%
Rum Recorder 25.32 KiB 25.32 KiB 0 B 0.00%
Logs 57.93 KiB 58.55 KiB +631 B +1.06%
Rum Salesforce N/A 140.10 KiB N/A N/A N/A
Rum Slim 140.05 KiB 140.10 KiB +49 B +0.03%
Worker 22.96 KiB 22.96 KiB 0 B 0.00%
Rum Shopify N/A 206.86 KiB N/A N/A N/A
Rum-shopify Profiler N/A 8.43 KiB N/A N/A N/A
Rum-shopify Recorder N/A 3.74 KiB N/A N/A N/A

@ImaneLargou
ImaneLargou marked this pull request as draft August 11, 2026 10:10
@ImaneLargou

Copy link
Copy Markdown
Author

I have read the CLA Document and I hereby sign the CLA

@ImaneLargou ImaneLargou changed the title [WASM] Capture wasm module build_ids and enrich error events ✨ [RUM-16985] Capture wasm module build_ids and enrich error events Aug 11, 2026
// wrapper only resolves once both are done. This guarantees that an error
// thrown immediately by an exported function can reference the loaded module.
function captureFromResponse(response: Response): Promise<void> {
const url = response.url || '<wasm-instantiate-streaming-no-url>'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏How could the URL be empty? Is this just because of the types?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It can be empty when the response is created manually with new Response(wasmBytes) instead of coming from fetch(). This is probably uncommon, but the fallback handles that edge case. I can remove the fallback if we don’t want to send a synthetic URL.

Comment on lines +85 to +102
trackingClients += 1
if (!stopTracking) {
stopTracking = installWasmModuleTracking()
}

let stopped = false
return () => {
if (stopped) {
return
}
stopped = true
trackingClients -= 1
if (trackingClients === 0) {
stopTracking?.()
stopTracking = undefined
registry.clear()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏How many clients could we expect tracking WebAssembly?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once the module calls are removed, we expect at most two clients: RUM and Logs.

import { makeLogsPublicApi } from '../boot/logsPublicApi'

// Install WebAssembly hooks before deferred Logs initialization so eagerly loaded modules are captured.
startWasmModuleTracking()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔨 warning: ‏The philosophy of the SDK is not to override any APIs unless we are allowed to track the user.

This should be deferred until SDK initialization.

// reads it to set source_type='browser+wasm' and error.wasm_modules.
// Must start before any wasm load — RUM is initialised before the page's
// wasm fetch in typical setups.
const stopWasmModuleTracking = startWasmModuleTracking()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❓ question: ‏Are we not initializing this in different places?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this was part of my first tests at wasm module tracking but modules loaded before startRum runs were missed. Forgot to remove it 😅. I'll delete it.

// wasm modules — by the time the deferred wrap installs, instantiateStreaming
// may have already been called. Installing here (before any deferral) closes
// that race.
startWasmModuleTracking()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔨 warning: ‏Mentioned before.

import { makeProfilerApiStub } from '../boot/stubProfilerApi'

// Install WebAssembly hooks before deferred RUM initialization so eagerly loaded modules are captured.
startWasmModuleTracking()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔨 warning: ‏Keeping track of all the places where this is automatically tracked.

…-experiment

# Conflicts:
#	package.json
#	packages/browser-core/src/domain/telemetry/telemetryEvent.types.ts
#	yarn.lock
…-experiment

# Conflicts:
#	package.json
#	packages/browser-logs/src/domain/runtimeError/runtimeErrorCollection.spec.ts
#	packages/browser-logs/src/rawLogsEvent.types.ts
#	yarn.lock
@ImaneLargou
ImaneLargou marked this pull request as ready for review August 31, 2026 11:23
trackingConsentState.onGrantedOnce(() => {
startTrackingConsentContext(hooks, trackingConsentState)
mockable(startTelemetry)(TelemetryService.LOGS, configuration, hooks.assembleTelemetry, sdkName)
stopWasmModuleTracking = mockable(startWasmModuleTracking)()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why do we start module tracking here?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It starts after consent but before the asynchronous session-manager startup to avoid missing modules loaded during that wait. When I was testing with rum, it seemed like modules were missing if we start later than this.

Comment thread packages/browser-core/src/domain/wasmModules/wasmModuleTracking.ts Outdated
// Hook 1: instantiate(bytes | module, imports). For raw bytes, we can read
// build_id directly; for an already-compiled WebAssembly.Module we have no
// URL or bytes to inspect — register a placeholder.
WebAssembly.instantiate = function (this: typeof WebAssembly, source: any, importObject?: any) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: use instrumentMethod to instrument all methods.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed to instrumentMethod

// Streaming compilation and metadata extraction happen in parallel, but the
// wrapper only resolves once both are done. This guarantees that an error
// thrown immediately by an exported function can reference the loaded module.
function captureFromResponse(response: Response): Promise<void> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: this might be better written as an async function

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function has been removed. We now extract the build ID directly from the compiled WebAssembly.Module, so we no longer clone or read the response body.

WebAssembly.compileStreaming = function (source) {
return Promise.resolve(source).then((response: Response) => {
const capturePromise = captureFromResponse(response)
return Promise.all([origCompileStreaming.call(this, response), capturePromise]).then(([module]) => module)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: using instrumentMethod here would make sure all received argument are passed to the original compileStreaming method (mdn shows that it supports a compileOptions object)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed it to use instrumentMethod!

try {
return response
.clone()
.arrayBuffer()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: you are downloading the whole response in memory -- that will have an impact on large wasm files. Would it be possible to read only a limited amount of bytes instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the PR so now we extract the build ID directly from the compiled WebAssembly.Module, and we no longer clone or read the response body.

await page.evaluate(async () => {
const { instance } = await WebAssembly.instantiateStreaming(fetch('/test-module.wasm'))

setTimeout(() => (instance.exports.run as () => void)())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question: why do we need this setTimeout?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The setTimeout makes the WASM trap occur as an uncaught browser error. If we call run() directly inside page.evaluate(), the exception is reported as an evaluation failure instead.

Comment thread test/e2e/scenario/logs.scenario.ts Outdated
})

createTest('send WebAssembly runtime errors with module metadata')
.withRum()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: don't add a rum test in logs.scenario. Move the test in a common scenario file, or add split the test (one for rum, one for logs)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests are split into one for logs and one for rum now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants