fix: sanitize page URLs recorded into telemetry - #729
Conversation
`ErrorMessage.url` was set from `window.location.href` verbatim, so any secret in the query string or fragment — OAuth `access_token`/`id_token`, magic links, password reset tokens — was uploaded with the error event. `sanitizeUrl` already redacts sensitive query params and strips token-bearing fragments (#595), but it was only wired into the network listener, never the error path. Add `sanitizedLocationHref()` and route every telemetry-bound page URL through it: - error URLs: `Highlight._recordErrorMessage`, `ObserveSDK` (x2), `ErrorListener` (window.onerror / unhandled rejection), and the `console.error` capture in `FirstLoadListeners` - span attributes: `url.full` on user-interaction and `ld.track` spans, `event.url`, and the page-view span's `url.full` / `page_view.url` / `page_view.previous_url` - the history-navigation span name in `_updateInteractionName` - metric `group` attributes across viewport, device, web-vital, performance, and network-performance gauges in both SDK generations - `Navigate` / `Reload` / `Referrer` custom events and the `referrer` session property - the jank listener's emitted `newLocation` Change detection and comparisons keep reading the raw `window.location.href` so redaction never collapses two distinct URLs; only the recorded value is sanitized. `window.location.pathname` sites are left alone — they carry no query or fragment. Note: metric `group` values and page-view URLs are now redacted, so aggregation keys change for URLs that contained sensitive params.
770cc64 to
dc66458
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit dc66458. Configure here.
| if (url !== urlAfter) { | ||
| plugin._updateInteractionName(urlAfter) | ||
| // Compare the raw URLs, but keep tokens out of the span name. | ||
| plugin._updateInteractionName(sanitizeUrl(urlAfter)) |
There was a problem hiding this comment.
History span name sanitization gap
High Severity
_patchHistoryMethod builds urlAfter as pathname + hash + search, then passes that string to sanitizeUrl. That order puts the query after the fragment, so the parser treats query params as part of the hash. Token-bearing fragments can survive when a benign query is present, and sensitive query keys like sig can miss redaction when a hash exists. Elsewhere this PR records sanitizedLocationHref() instead.
Triggered by learned rule: URLs emitted to telemetry must go through sanitizeUrl — covers fragments and OAuth query params
Reviewed by Cursor Bugbot for commit dc66458. Configure here.
There was a problem hiding this comment.
Confirmed and fixed in 09f73d4.
Reproduced both halves against the real URL parser:
| composed string | parsed hash |
parsed search |
leak |
|---|---|---|---|
/callback#access_token=SECRET?foo=bar |
#access_token=SECRET?foo=bar |
"" |
sanitizeFragment splits on the ? and only inspects foo=bar, so the token survives |
/p#section?sig=SECRET |
#section?sig=SECRET |
"" |
search is empty so query redaction never runs, and sig is not in SENSITIVE_FRAGMENT_PARAMS |
Fix is to compose pathname + search + hash so the string is a well-formed URL. Both url and urlAfter use the same order, so the raw comparison that gates the rename is unaffected.
Added user-interaction.test.ts, which drives the real patched history.pushState and asserts on the span name. Verified it is genuine regression coverage — with the old ordering restored, 3 of the 4 cases fail (/download#section?sig=SECRET, /users#section?page=2&sort=name, and the access_token case); with the fix they pass.
yarn turbo run lint enforce-size test --filter highlight.run: 26 files, 456 tests pass. format-check and tsc --noEmit clean.
`_patchHistoryMethod` composed the URL as `pathname + hash + search`, which is not a well-formed URL. Passing it to `sanitizeUrl` made the URL parser absorb the query into the fragment, defeating redaction in both directions: - a token-bearing fragment survived whenever a benign query was also present, because `sanitizeFragment` split on the `?` that had been appended after the hash and only inspected the trailing query params - sensitive query keys that are not also fragment-sensitive (`sig`, `signature`, `awsaccesskeyid`) were never redacted when a fragment was present, since `urlObject.search` came back empty Composing `pathname + search + hash` fixes both. The comparison that gates the rename still uses the raw values and both sides use the same order, so change detection is unaffected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>


Summary
ErrorMessage.urlwas set fromwindow.location.hrefverbatim, so any secret sitting in the query string or fragment (OAuthaccess_token/id_token, magic links, password reset tokens) was uploaded to the backend with the error event.sanitizeUrlalready redacts sensitive query params and strips token-bearing fragments — that landed in #595 — but it was only ever wired into the network listener. The error path is separate code (client/index.tsx, notnetwork-sanitizer.ts) and was never covered.This adds
sanitizedLocationHref()next tosanitizeUrland routes every telemetry-bound page URL through it.What changed
Error URLs
Highlight._recordErrorMessage—client/index.tsxObserveSDKerror recording, both call sites —sdk/observe.tsErrorListener—window.onerrorand unhandled rejectionsconsole.errorcapture inFirstLoadListenersSpan attributes and names
url.fullandevent.urlon user-interaction spansurl.fullonld.trackspansurl.full,page_view.url,page_view.previous_urlon the page-view span — this one fires on every SPA navigation, including OAuth callbacks_updateInteractionNameMetric
groupattributes across viewport, device, web-vital, performance, and network-performance gauges, in both SDK generations (client/index.tsxandsdk/observe.ts) plusH.recordMetricCustom events and session properties
Navigate/Reloadcustom eventsReferrercustom event andreferrersession property (document.referrercarries the previous page's query string)newLocationDeliberately unchanged
window.location.href(LocationChangeInstrumentation._lastUrl,jankState.location, the pre/post URLs in_patchHistoryMethod). Sanitizing those would let redaction collapse two distinct URLs into one and silently drop legitimate page views. Only the recorded value is sanitized.window.location.pathnamesites — no query or fragment to leak.SegmentIntegrationListener's initialcallback(window.location.href). Its consumers branch onobj.type, so a bare string is dropped and never recorded.How did you test this change?
error-listener.test.tsdrives the realwindow.onerrorhandler with?access_token=…and#id_token=…in the URL and asserts the secret is absent from the recordedErrorMessage.url, plus a case confirming benign paths, params, and anchors survive.sanitizedLocationHrefcases alongside the existingsanitizeUrlsuite.yarn turbo run lint enforce-size test --filter highlight.run— 25 files, 452 tests pass; bundle 169.83 kB brotli against the 256 kB limit.yarn format-checkandyarn dedupe --checkclean.Are there any deployment considerations?
Patch-level. One behavior change worth flagging: metric
groupvalues and page-view URLs are now redacted, so aggregation keys shift for any URL that contained sensitive params. Metrics from such URLs will group under the redacted form rather than the raw one.🤖 Generated with Claude Code
Note
Overview
Stops secrets in query strings and URL fragments (OAuth tokens, magic links, etc.) from being uploaded with errors, spans, metrics, and session replay custom events by routing telemetry-bound page URLs through existing
sanitizeUrllogic via a newsanitizedLocationHref()helper.Errors and listeners:
ErrorMessage.urland console-captured errors now usesanitizedLocationHref()instead of rawwindow.location.hrefin the main client, Observe SDK,ErrorListener, andFirstLoadListeners.Spans and analytics: User-interaction spans (
url.full,event.url), page-view spans on SPA navigations (url.full,page_view.url,page_view.previous_url),ld.trackspans, and history-driven navigation span names are sanitized before recording; raw URLs are still used only for equality / change detection so redaction does not drop navigations.Metrics and replay: Metric
groupattributes (viewport, device, web vitals, performance,H.recordMetric) use the sanitized href.Navigate/Reloadcustom events,Referrerevents andreferrersession properties (sanitizeUrl(document.referrer)), and janknewLocationare redacted before recording.Tests: New
error-listener.test.tsandsanitizedLocationHrefcases in the OTel instrumentation suite.Reviewed by Cursor Bugbot for commit dc66458. Bugbot is set up for automated code reviews on this repo. Configure here.