Skip to content

feat(browser): error filters and linked error causes - #1146

Merged
Makisuo merged 4 commits into
feat/browser-sdk-logs-samplingfrom
feat/browser-sdk-error-filters
Sep 29, 2026
Merged

Makisuo merged 4 commits into
feat/browser-sdk-logs-samplingfrom
feat/browser-sdk-error-filters

Conversation

@Makisuo

@Makisuo Makisuo commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Part 2 of the browser SDK stack. Based on #1145.

Errors a customer's page throws are not all theirs to fix: browser extensions inject scripts, and ResizeObserver reports benign loop notices. Until now there was no way to drop them, and each one became an issue.

What changes

Filters (errors option), checked before an error span exists, on both the global handlers and captureException:

  • ignore: strings / RegExps matched against Name: message.
  • denyUrls / allowUrls: matched against the top frame's script URL, parsed from V8 and SpiderMonkey/JavaScriptCore stacks (window.onerror's filename when there is no stack). Errors with no frames are kept by allowUrls.
  • beforeCapture(error, { source, originalError }): return false to drop. A hook that throws keeps the error.
  • Defaults: extension frames (chrome-extension://, moz-extension://, safari-web-extension://, ...) and the ResizeObserver loop notices are dropped. errors.defaultFilters: false keeps them.

Filtering happens at capture time, not in a span processor, because a SpanProcessor cannot drop a span, and a dropped error should cost nothing.

Linked errors

  • error.cause chains and AggregateError members (up to five, cycle-safe) are appended to exception.stacktrace as Caused by: blocks after the error's own frames, the way OTel Java records a Throwable.
  • Fingerprints hash the top three frames (error_events_mv), so only errors with fewer than three frames of their own can move to a new issue.
  • The error object is handed to recordException untouched when nothing is linked, and code is kept otherwise (OTel prefers it over name for exception.type), so exception.type never changes.

Eager budget 41 → 42 kB and first-party 16 → 17 kB: this code runs on the capture path.

Testing

  • Unit tests for frame URL parsing (V8 and Firefox/Safari formats, URLs in the message line), every filter, the hook's failure mode, cause rendering (chains, non-Error causes, aggregates, cycles, depth cap, code preserved).
  • Browser tests for the real capture path: a filtered captureException, an extension-thrown uncaught error, and the cause chain landing in exception.stacktrace.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Summary by CodeRabbit

  • New Features
    • Added configurable browser error filtering by message, script URL, and a custom capture check.
    • Browser-extension errors and common ResizeObserver loop notices are filtered by default; default filters can be disabled.
    • Exception reports now include linked cause and AggregateError entries, up to five linked errors.
  • Documentation
    • Added guidance on error-filter options, default filters, linked errors, and their effect on fingerprinting.

Errors a customer's page throws are not all theirs to fix: browser
extensions inject scripts, and ResizeObserver reports benign loop notices.
They had no way to drop those, and every one became an issue.

- errors.ignore / denyUrls / allowUrls / beforeCapture, checked before an
  error span exists, on both the global handlers and captureException.
  URL lists match the top frame's script URL, parsed from V8 and
  SpiderMonkey/JavaScriptCore stacks (window.onerror's filename when there
  is no stack).
- Extension frames and ResizeObserver loop notices are dropped by default;
  errors.defaultFilters: false keeps them.
- error.cause chains and AggregateError members (up to five) are appended
  to exception.stacktrace as "Caused by:" blocks after the error's own
  frames. Fingerprints hash the top three frames, so only errors with
  fewer than three frames of their own can move to a new issue. The error
  object is passed through untouched when nothing is linked, and `code` is
  kept otherwise, so exception.type never changes.
- Eager budget 41 -> 42 kB, first-party 16 -> 17 kB for the capture-path code.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 3/5 · needs attention
The change silently drops extension and ResizeObserver errors for every existing browser app by default, and the new filename fallback never runs in a browser.
quality 90/100 · 1 warning · tests covered · risk medium

Adds a configurable errors filter block (ignore lists, URL allow/deny, beforeCapture, default extension/ResizeObserver drops) plus error.cause/AggregateError rendering into exception.stacktrace, gated on the capture path before any span exists. Well tested, but one URL-filter defect is worth fixing before merge.

  • errors config adds ignore, denyUrls, allowUrls, beforeCapture, defaultFilters
  • Extension and ResizeObserver loop errors are dropped by default before a span exists
  • error.cause chains and AggregateError members are appended as Caused by: blocks
  • exceptionOf keeps code when it rewrites an error's stack

Findings

Warning · F1 · denyUrls/allowUrls judge a fabricated stack frame, so filename never applies

correctness · packages/browser/src/error-filters.ts:58

recordException filters asError(error) (errors.ts:87), and asError wraps any non-Error value — a string rejection, captureException("...") — in new Error(value), whose stack's top frame is the SDK's own script URL. frameUrls therefore always returns a URL for those values, so topUrl is never empty and the ?? filename fallback is dead in a real browser: on Firefox a window.onerror placeholder is judged on the maple bundle instead of event.filename, and with allowUrls set a string rejection is dropped on the bundle's URL rather than kept as the documented frame-less error. The unit test only shows the fallback working because it assigns stack = undefined by hand (error-filters.test.ts:13), which no browser does.

Apply the URL lists only to frames that belong to the captured value: when `error` is not an `Error` (nothing of the page's was thrown) skip URL matching, and for the `window.onerror` placeholder pass `event.filename` as the frame URL explicitly instead of relying on the synthesized stack.
What was checked
  • Cause rendering is bounded and cycle-safe: seen is seeded with the error and MAX_LINKED is 5 (error-causes.ts:11-24)
  • Fingerprint inputs stay put for errors with own frames: the MV slices the top 3 frame-shaped lines (materializations.ts:798-815)
  • Filters are configured before setupErrorCapture() registers handlers (init.ts:85, init.ts:130), and reset on shutdown
Copy all findings (1)
Findings from an automated review of commit 1994d1d3619757f3108a9a9c52420b2ff9eae64c. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F1 · Warning · correctness · packages/browser/src/error-filters.ts:58
`denyUrls`/`allowUrls` judge a fabricated stack frame, so `filename` never applies
`recordException` filters `asError(error)` (`errors.ts:87`), and `asError` wraps any non-`Error` value — a string rejection, `captureException("...")` — in `new Error(value)`, whose stack's top frame is the SDK's own script URL. `frameUrls` therefore always returns a URL for those values, so `topUrl` is never empty and the `?? filename` fallback is dead in a real browser: on Firefox a `window.onerror` placeholder is judged on the maple bundle instead of `event.filename`, and with `allowUrls` set a string rejection is dropped on the bundle's URL rather than kept as the documented frame-less error. The unit test only shows the fallback working because it assigns `stack = undefined` by hand (`error-filters.test.ts:13`), which no browser does.
Suggested fix: Apply the URL lists only to frames that belong to the captured value: when `error` is not an `Error` (nothing of the page's was thrown) skip URL matching, and for the `window.onerror` placeholder pass `event.filename` as the frame URL explicitly instead of relying on the synthesized stack.

1994d1d · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@coderabbitai

coderabbitai Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 5bd69ef0-449d-41fc-9cd6-779df1589492

📥 Commits

Reviewing files that changed from the base of the PR and between 43a9cc9 and c095d88.

📒 Files selected for processing (13)
  • docs/browser-sdk.md
  • packages/browser/scripts/size.ts
  • packages/browser/src/config.ts
  • packages/browser/src/error-causes.test.ts
  • packages/browser/src/error-causes.ts
  • packages/browser/src/error-filters.test.ts
  • packages/browser/src/error-filters.ts
  • packages/browser/src/errors.browser.test.ts
  • packages/browser/src/errors.ts
  • packages/browser/src/index.ts
  • packages/browser/src/init.ts
  • packages/browser/src/navigation.test.ts
  • packages/browser/src/tracing.browser.test.ts

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 1 remain after this review.


📝 Walkthrough

Walkthrough

The browser SDK adds configurable error filters for captured errors and formats linked causes in exception stacks. Capture handlers apply the filters before recording errors. Configuration, initialization, tests, documentation, and size budgets are updated to cover these changes.

Changes

Browser error capture

Layer / File(s) Summary
Error filter configuration
packages/browser/src/config.ts, packages/browser/src/error-filters.ts, packages/browser/src/init.ts, packages/browser/src/index.ts, packages/browser/src/navigation.test.ts, packages/browser/src/tracing.browser.test.ts, docs/browser-sdk.md
The browser configuration accepts errors options and resolves them to errorFilters. Initialization configures and clears the filters. The package exports the filter types, and the configuration table documents the option.
Error filtering
packages/browser/src/error-filters.ts, packages/browser/src/error-filters.test.ts
Filtering extracts stack URLs and applies default filters, message ignores, URL rules, and beforeCapture. Tests cover the filtering rules and callback behavior.
Cause-chain stack formatting
packages/browser/src/error-causes.ts, packages/browser/src/error-causes.test.ts
Cause and AggregateError values are appended to exception stacks, with traversal limited to five linked values. Tests cover ordering, cycles, non-Error values, and code preservation.
Capture-path integration
packages/browser/src/errors.ts, packages/browser/src/errors.browser.test.ts, docs/browser-sdk.md, packages/browser/scripts/size.ts
Capture handlers pass source and available filename context to filtering. Recorded exceptions use cause-chain formatting. Browser tests, SDK documentation, and size budgets cover the added behavior.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~20 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant CaptureEntryPoints
  participant recordException
  participant shouldCapture
  participant recordFailure
  participant exceptionOf
  participant OpenTelemetry
  CaptureEntryPoints->>recordException: Pass source, error, and optional filename
  recordException->>shouldCapture: Check normalized error and capture context
  shouldCapture-->>recordException: Return capture decision
  recordException->>recordFailure: Record accepted error
  recordFailure->>exceptionOf: Build exception with cause stack
  exceptionOf-->>recordFailure: Return exception representation
  recordFailure->>OpenTelemetry: Record exception
Loading

Suggested reviewers: jeremyfunk

Merge Risk: ⚪ Minimal · up to c095d

Error filtering and linked-cause reporting appear consistent with the documented behavior. No actionable merge-blocking defect was established; merge after normal checks pass.

Security Architecture Review

Security architecture risk: 🔵 Low · up to c095d

The new filters remain within the application’s existing error-reporting boundary. Linked-error formatting introduces a bounded diagnostic failure risk, and it expands the information included in reports. No privilege escalation or cross-tenant exposure was established.

Retained concerns

  • Low · reliability · inferred: Linked-error enrichment is not isolated from capture failure. Aggregate members are spread before the five-value limit is applied, and non-Error linked values are stringified without a fallback. If either operation throws, the original error may already be marked as reported, while exception recording and span completion are skipped. Subsequent capture of that same object can then be suppressed. This introduces a diagnostic failure-containment regression, although attacker-controlled production reachability was not established.
Security review details

Security Blast Radius

  • inferred — The demonstrated impact concerns errors captured within an instrumented browser application and their existing telemetry destination. Filter configuration is shared within the SDK singleton. The inspected change does not demonstrate additional service privileges or cross-tenant authority.

Security Findings and Attack Paths

  • inferred — An accepted Error with an unrenderable linked value can interrupt reporting during enrichment. Its identity can already be recorded in the deduplication set, preventing a later attempt from recovering the diagnostic. Exploitation would require influence over linked error values in the application; such an attacker-controlled production path was not demonstrated.

Trust Boundaries and Controls

  • observed — The filters govern telemetry selection rather than authenticate script origin. They match error text and parsed stack URLs, retain errors without frame URLs, and keep errors when the callback throws. All inspected manual and global capture paths use this same gate.

Resilience and Maintainability Implications

  • inferred — The callback runs before an error identity is claimed. A callback that synchronously recaptures the same error can therefore re-enter filtering recursively. Callback exceptions are caught, but that protection does not prevent reentry. This requires application-supplied callback behavior and is not established as an attacker-facing bypass.

Hardening Proposals

  • proposed — Make linked enrichment best-effort: limit aggregate reads before materialization, contain conversion failures, and preserve recording of the outer error when enrichment fails. Treat linked text as an expansion of the telemetry data contract when defining redaction and payload limits.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 12 files. (1 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the two main changes: browser error filters and linked error causes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 14 functions across 12 files. (1 skipped: 1 unsupported.)

  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@maple-review-bot maple-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 inline note from Maple's review. The score and summary are in the review comment above.

Comment thread packages/browser/src/error-filters.ts Outdated
…arries

An Error wrapped around a non-Error value (a string rejection,
captureException("...")) has this SDK's frames, so denyUrls/allowUrls
matched Maple's bundle and window.onerror's filename was never used. Only a
thrown Error's own stack is matched now; window.onerror passes its filename
as the frame when no Error was thrown.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
F1 is fixed and the filter and cause paths are unit- and browser-tested; the one defect is a caller-supplied /g regex mis-matching.
quality 98/100 · 1 note · tests covered · risk medium

Adds client-side error filters (ignore, denyUrls/allowUrls, beforeCapture, extension and ResizeObserver defaults) checked before a span exists, and appends error.cause/AggregateError chains to exception.stacktrace. Safe to merge; the fixes for the earlier URL-list finding hold.

  • shouldCapture runs filter lists and beforeCapture before the error span is created
  • error.cause and AggregateError members land in exception.stacktrace as Caused by: blocks
  • errors option on MapleBrowser.init, reset on teardown, re-exported as types
  • URL lists now read frames only from a thrown Error; otherwise window.onerror's filename

Findings

Note · F2 · A /g regex in ignore/denyUrls/allowUrls matches only every other error

correctness · packages/browser/src/error-filters.ts:47

matches calls pattern.test(value) on the caller's own RegExp, and a global regex keeps lastIndex between calls: with ignore: [/chunk/g] the first ChunkLoadError is dropped, the second is reported, and the alternation continues for the life of the page (verified with bun -e on (v,p) => p.some(x => x.test(v))). Reset lastIndex before testing, as in the replacement.

const matches = (value: string, patterns: ReadonlyArray<string | RegExp>): boolean =>
	patterns.some((pattern) => {
		if (typeof pattern === "string") return value.includes(pattern)
		pattern.lastIndex = 0
		return pattern.test(value)
	})

Fixed since the last review

  • F1 · denyUrls/allowUrls judge a fabricated stack frame, so filename never applies
What was checked
  • F1 fixed: shouldCapture reads frames only for an Error original (error-filters.ts:63) and window.onerror passes filename only when no Error was thrown (errors.ts:150)
  • error_events_mv selects frame lines by shape (^[ \t]*at in migrations/0030), so Caused by: lines cannot enter the fingerprint hash
  • frameUrls against V8, SpiderMonkey and a blob: frame ran with bun -e: URL-only frames in the message line are skipped
Copy all findings (1)
Findings from an automated review of commit 4849cd648d77bd20f3e503699b900fad72cc0a3a. Verify each one against the current code before changing anything, fix only those that still apply, and keep each fix to the lines it names.

---

F2 · Note · correctness · packages/browser/src/error-filters.ts:47
A `/g` regex in `ignore`/`denyUrls`/`allowUrls` matches only every other error
`matches` calls `pattern.test(value)` on the caller's own RegExp, and a global regex keeps `lastIndex` between calls: with `ignore: [/chunk/g]` the first `ChunkLoadError` is dropped, the second is reported, and the alternation continues for the life of the page (verified with `bun -e` on `(v,p) => p.some(x => x.test(v))`). Reset `lastIndex` before testing, as in the replacement.
Replace those lines with:
const matches = (value: string, patterns: ReadonlyArray<string | RegExp>): boolean =>
	patterns.some((pattern) => {
		if (typeof pattern === "string") return value.includes(pattern)
		pattern.lastIndex = 0
		return pattern.test(value)
	})

4849cd6 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

@Makisuo
Makisuo added this pull request to stack #1161 September 29, 2026 21:22
A g/y RegExp keeps lastIndex between test() calls, so ignore/denyUrls/
allowUrls with /chunk/g dropped only every other matching error. Reset
lastIndex before each test.
@maple-review-bot

maple-review-bot Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Maple review

Confidence 4/5 · likely safe to merge
F2's fix is real and the new filter path is unit- and browser-tested; only the frame-URL regex's less common formats are untested.
quality 100/100 · no findings · tests covered · risk medium

Adds errors capture filters (ignore/denyUrls/allowUrls/beforeCapture plus default extension and ResizeObserver drops) evaluated before the exception span is created, and appends cause/AggregateError chains to exception.stacktrace. The global-regex fix at this head is correct; safe to merge.

  • shouldCapture drops ignored, extension and ResizeObserver errors before recordException starts a span
  • configureErrorFilters wires config.errors through init and the runtime shutdown
  • exceptionOf appends Caused by: blocks from cause and AggregateError chains to exception.stacktrace
  • window.onerror passes event.filename as the top frame when no Error was thrown

Fixed since the last review

  • F2 · A /g regex in ignore/denyUrls/allowUrls matches only every other error
What was checked
  • matches resets pattern.lastIndex before test (error-filters.ts:50), so /g ignore lists drop every match — F2's defect is gone
  • FRAME_URL reads V8 and SpiderMonkey frames and not a URL in the message line (error-filters.test.ts:23, :34)
  • frameUrl from window.onerror takes precedence over a wrapped Error's SDK stack (error-filters.ts:68, errors.ts:150)

c095d88 · Updated on every push. Reply "won't fix" to dismiss a finding, or mention @maple to ask about one.

if (!stack) return []
const urls: string[] = []
for (const line of stack.split("\n")) {
const url = FRAME_URL.exec(line)?.[1]
@Makisuo
Makisuo merged commit eb7e6b3 into main Sep 29, 2026
36 of 37 checks passed
@Makisuo
Makisuo deleted the feat/browser-sdk-error-filters branch September 29, 2026 21:44
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.

2 participants