Repository navigation
Conversation
Add Exception Replay to the Browser Debugger SDK. Code built with the Live Debugger build plugin's `exceptionReplay` option calls `$dd_catch` for every exception that leaves an instrumented function. The SDK captures the arguments and `this` of the three innermost frames, keeps them with the error, and only sends them as snapshots when RUM reports the error. A new RUM plugin, `datadogDebugger.exceptionReplayPlugin()`, adds `error.debug_info_captured` and `_dd.exception_replay` to the RUM error event to link it to its snapshots. Captures redact sensitive names using the tracers' list, are limited to 10 new exceptions per second and one report per error type and function per hour, and count toward the global snapshot limit. Environment: Datadog workspace Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
❌ ErrorsYour PR has failed checks. Please review the issues below and take necessary action before merging. 🚦 2 Pipeline jobs failed
ℹ️ InfoNo other issues found (see more)❄️ No new flaky tests detected 🎯 Code Coverage (details) Useful? React with 👍 / 👎 This comment will be updated automatically if new data arrives.🔗 Commit SHA: df1a364 | Docs | View more details | Give us feedback! |
Bundles Sizes Evolution
|
Exception Replay sent an error's snapshots while RUM assembled the error event, before `beforeSend` and the event rate limits ran. An error that either of them dropped still sent its snapshots, with no error event to link them to. Add an `eventCollected` hook to RUM, triggered once an event has passed `beforeSend` and the rate limits, and expose it to plugins as `registerEventCollectedHook`. The Exception Replay plugin still adds its attributes while the event is assembled, but holds the snapshots until the hook fires. The one-hour fingerprint limit also starts then. Also stop capturing in sessions RUM doesn't track, since their errors are never reported, and fix the async function spec on Safari 14, which leaves the function's name out of the stack after an `await`. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The declaration build failed with "Type instantiation is excessively deep" where the `eventCollected` hook is triggered. Checking that the domain context fits `DeepReadonly` recursed through the DOM types it holds. `yarn typecheck` didn't catch it, as it uses another configuration. The hook's domain context is now a plain `RumEventDomainContext`. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ay schema PR check-schemas regenerates the types from the pinned schema, which didn't have the Exception Replay fields yet. Pin the head commit of DataDog/rum-events-format#458 until it merges, then bump to its merge commit. The regenerated types also pick up #454 (Add Unreal as source), which is already on rum-events-format master. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Exception Replay captures on the main thread as exceptions are thrown, and the 10 ms per-frame deadline alone allowed up to 300 ms per second (10 exceptions with 3 frames each). The deadline also can't interrupt a single slow step, such as reading the property names of an object with 100k keys. - Cap capturing at 25 ms per second on average. Each frame's deadline is also capped by the remaining budget. Time spent beyond the budget is carried over, so capturing pauses until it's made up. - Stop counting Exception Replay snapshots against the probes' maxSnapshotsPerSecondGlobally, so neither feature can starve the other. Exception Replay is already limited by its own caps. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
Exception Replay can attach arguments to unrelated async stack frames, omit valid captures, and upload sensitive symbol-property values without redaction. Serialization failures and retries of previously dropped errors can also bypass capture limits.
🤖 Bits Code Review · Commit d17501a
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d17501a1aa
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Redact properties and map entries with sensitive symbol keys, which are captured under their description. - Capture errors thrown before RUM starts, whose session isn't known yet. Only skip sessions that RUM doesn't track once it has started. - Don't send the snapshots of an error that RUM dropped once another error with the same fingerprint has been reported. - Trigger the `eventCollected` hook before the event is dispatched, as documented. - Charge the capture time budget even when capturing throws. - Compare only the line of async caller frames, which are located at the `await` instead of the call, so outer async frames aren't dropped. - Match a frame without callers only to a frame of the same function, so its snapshot can't land on an unrelated frame of the same script. - Document that a `beforeSend` removing or reordering frames in `error.stack` can misplace snapshots. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t async stack traces The older Chrome, Edge, Firefox and Safari versions on BrowserStack don't record async stack traces, so once `asyncCaller` resumes, its stack shows no caller and its frame can't be confirmed. The SDK then leaves it without a snapshot, as intended. Only expect its snapshot when the browser records async stack traces. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Snapshot links can point to the wrong frame in single-line async bundles or to nonexistent frames when a thrown plain object is reported through RUM. Both defects have localized fixes.
🤖 Bits Code Review · Commit 5baae08
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8db607da74
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- Don't capture before RUM starts while tracking consent isn't granted. - Only capture `Error` instances: RUM doesn't report the stack of other thrown values, so their snapshots would link to frames that aren't there. - Keep function identity when comparing async caller frames by line, so two functions on the same line of a minified bundle don't match. - Report redacted objects as `Object` or `Array`, without reading their constructor name, which the redacted object controls. - Document that RUM and the Live Debugger SDK must send to the same site and organization. - Make the async callee spec hold in browsers that don't show async callers on the line of their `await` (Chrome 80, Edge) or don't name async functions (Safari 14): check frame positions, and only require the caller's snapshot where the browser shows the caller there. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9cbf27e05e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Brings in the addPlugin() RUM API, and main with #5217. The conflict in preStartRum.ts is resolved by adding registerEventCollectedHook to the options that both the init plugins and the added plugins get. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Customers no longer pass a plugin to DD_RUM.init(), so the Live Debugger SDK doesn't have to load before RUM is initialized. DD_DEBUGGER.init() adds the Exception Replay plugin with DD_RUM.addPlugin() once the RUM Browser SDK has loaded, including with the async loading snippet. It warns if that version of RUM can't add plugins after init. - Remove exceptionReplayPlugin() from the public API. - Update the README, and the e2e tests, which no longer have to start RUM from DD_DEBUGGER.onReady(). Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Exception Replay can retain arguments captured while effective RUM consent is denied and send them when the same Error is reported after consent is granted. Anonymous async catches can also attach snapshots to unrelated stack frames.
🤖 Bits Code Review · Commit f72c087
💡 Codex Reviewbrowser-sdk/packages/browser-rum-core/src/boot/preStartRum.ts Lines 319 to 320 in f72c087 issue: When a plugin supplied to browser-sdk/packages/browser-rum-core/src/boot/preStartRum.ts Lines 253 to 256 in f72c087 issue: When an added plugin's ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…Replay Before RUM started, Exception Replay only checked the trackingConsent init option. It missed consent denied with setTrackingConsent() before init(), which overrides that option, and so captured arguments while consent was denied. - RUM gives plugins isTrackingConsentGranted(), which reads the current consent, however it was set. - Exception Replay uses it until RUM starts, and stays off with a RUM version that lacks it. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bfa04bba24
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A function without callers was matched to a frame of the same script that came earlier in the file whenever one of the two had no function name. An anonymous function that caught an error created by a named function of the same script was then linked to that function's frame. A frame with a name and one without are now from different functions. Two unnamed functions of the same script can still be mistaken for each other. Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Note 🤖 Claude Opus 5.5 responding on behalf of Thomas Watson Replying to the Codex review above: both findings are fixed in #5220 (26f1779) and merged here in aa5ab4d.
|
…5220) Environment: Datadog workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: df1a3640ac
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const snapshots = exception.pendingSnapshots | ||
| exception.pendingSnapshots = undefined | ||
| for (const snapshot of snapshots) { | ||
| queueExceptionSnapshot(snapshot) | ||
| } | ||
| recordReport(exception.fingerprint) |
There was a problem hiding this comment.
Recheck the cooldown before sending pending snapshots
issue: When a beforeSend callback synchronously captures and reports another error with the same fingerprint, the nested event can send first and start the cooldown while this outer event still has pending snapshots. This path sends those outer snapshots without rechecking isCoolingDown(), so both exceptions receive snapshots despite the documented once-per-hour limit. Recheck the fingerprint immediately before queueing the pending snapshots.
Useful? React with 👍 / 👎.
| readonly domainContext: RumEventDomainContext<RawRumEvent['type']> | ||
| } | ||
|
|
||
| export type EventCollectedHook = Hook<EventCollectedHookParams, void> |
There was a problem hiding this comment.
Prevent collected hooks from suppressing later callbacks
issue: When an earlier plugin's collected callback returns DISCARDED, this Hook type explicitly permits that result and createHook().trigger() stops invoking subsequent callbacks, even though startRumAssembly() ignores the result and still dispatches the RUM event. If Exception Replay registered after that plugin, the error event is sent with snapshot links but its collected callback never queues the snapshots. This notification-style hook should invoke every registered callback rather than inherit assembly-hook discard semantics.
Useful? React with 👍 / 👎.
| registerExceptionReplayPlugin(() => | ||
| rumStarted ? publicApi.getInternalContext() !== undefined : isTrackingConsentGranted() | ||
| ) |
There was a problem hiding this comment.
Stop pre-start capture when RUM session setup fails
issue: When RUM accepts its configuration but startSessionManager() later resolves without a manager—for example, the default cookie persistence is unavailable—onRumStart is never called, yet this pre-start predicate keeps returning true while consent remains granted. $dd_catch then captures function arguments for every eligible exception indefinitely even though RUM has explicitly disabled event delivery and can never collect those snapshots. Fresh evidence beyond the previously fixed aborted-init path is the asynchronous no-storage return from startSessionManager() after added plugins have already initialized; the plugin needs a terminal initialization-failure signal that disables capture.
Useful? React with 👍 / 👎.
Motivation
DEBUG-6444
Exception Replay shows the arguments a function had when an error passed through it, on the error's stack frames in Error Tracking. Backend errors get it from the tracers, but browser errors have nothing like it. This brings it to browser errors reported by RUM, using the Live Debugger's build-time instrumentation. This first version captures arguments and
thisonly, not local variables.Changes
exceptionReplayoption calls$dd_catchfor every exception that leaves an instrumented function. The SDK captures the arguments andthisof the three innermost frames as the exception is thrown, and keeps them with the error in aWeakMap. Errors that RUM never reports are garbage collected with their captures, and nothing is sent for them.DD_DEBUGGER.init()adds a RUM plugin to the RUM Browser SDK withDD_RUM.addPlugin()(✨ [DEBUG-6444] Let SDKs add RUM plugins after init with addPlugin() #5220), once RUM has loaded, so customers don't change their RUM configuration, and the Live Debugger SDK can load after RUM is initialized. The plugin addserror.debug_info_captured: "true"and_dd.exception_replay(the exception ID, plus the stack index and snapshot ID of each captured frame) to the RUM error event while it's assembled. Each snapshot carriesexceptionId, so the UI treats it as an Exception Replay snapshot.beforeSendand the event rate limits. An error that either of them drops sends no snapshots, and doesn't start the one-hour limit for its fingerprint.RumPluginOnInitOptions.registerEventCollectedHookregisters a callback that runs once an event has passedbeforeSendand the rate limits, right before it's sent. It's backed by a neweventCollectedhook inhooks.ts, triggered inassembly.ts. A newisTrackingConsentGranted()option tells plugins whether tracking consent is granted, including consent set withsetTrackingConsent()beforeinit(). Until RUM starts, Exception Replay only captures while it is. With an older RUM SDK that lacks either option oraddPlugin(), Exception Replay warns and stays off.error.stackby lining up the call stack taken inside$dd_catchwith the error's stack, as parsed bycomputeStackTrace. Error Tracking attaches snapshots to frames by that position.maxSnapshotsPerSecondGlobally, so neither feature can use up the other's.exceptionReplay, and initialize both the RUM Browser SDK and the Live Debugger SDK.$dd_catchis only defined once RUM has initialized the plugin and the Live Debugger SDK is initialized. Nothing is captured in sessions that RUM doesn't track.browser-debugger. Specs check at compile time that it's still assignable toRumPlugin, and that the RUM public API still has theonReady()andaddPlugin()it uses.browser-rum-coreis only a devDependency, because declaring it as a dependency brokeyarn build:apps.This change was written with an AI agent. Please look closely at the frame-index matching in
exceptionReplay.ts, the RUM plugin's types, and the neweventCollectedhook, which the RUM team should review.Depends on the rum-events-format PR.
@datadog/rum-events-formatis pinned to the head commit of DataDog/rum-events-format#458, which adds the new fields. Once that PR merges, the pin has to be bumped to its merge commit before this one merges. The regenerated types also include #454 (Add Unreal as source), already on rum-events-format master.Stacked on #5220, which adds
addPlugin(). This PR's base is #5220's branch until it merges.Test instructions
yarn test:unit --spec packages/browser-debugger/src/domain/exceptionReplay.spec.ts. The plugin, redaction and capture specs sit next to it.yarn test:e2e -g "Exception Replay". One test checks the snapshots are linked to the RUM error event, the other that an error dropped bybeforeSendsends none. The test page uses hand-written code equivalent to what the build plugin generates, so it doesn't need a build-plugins release. Both pass in Chromium, Firefox and WebKit.Checklist
Related PRs
exceptionReplaybuild option that adds the$dd_catchcall.addPlugin()RUM API, which the SDK uses to add its RUM plugin. Merge before the SDK PR.addPlugin(), with the schema bump. (this PR)Created with Claude Opus 5.5 and Claude Sonnet 5.5 in Claude Code.