Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions docs/SENTRY.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,45 @@ Source maps do not change this posture: they reveal *code structure* (already
shipped publicly in minified form), never runtime data. Auth token and upload
traffic stay inside the build — nothing secret reaches the browser bundle.

## Foreign-noise filters

`sanitizeSentryEvent` also returns `null` for a small list of confirmed
injected/external noise signatures (`KNOWN_FOREIGN_NOISE` in `lib/sentry.ts`).
Every predicate is a conjunction of an exact error signature AND
foreign/vendor evidence — never a broad error class — and each has paired
unit tests proving the observed event drops while a similar legitimate
error retains. A global veto also applies: any `/_next/`, `webpack://` or
`gtm.js` frame anywhere in the event's exception chain keeps it
reporting, so a genuine app error can never be swallowed by a noise rule.

| Predicate | Drops | Requires |
|---|---|---|
| `isInjectedMediaFilterError` | extension media-filter errors | `DataCloneError` postMessage/`HTMLIFrameElement`/`could not be cloned` message + `mediafilter` frame |
| `isClarityIcuError` | Clarity `Internal error. Icu error.` | `RangeError` + exact message + `clarity.js` frame + `Intl.DateTimeFormat` frame |
| `isExtensionSendMessageError` | extension `runtime.sendMessage` failures | exact `Invalid call to runtime.sendMessage(). Tab not found` message + `auto.browser.global_handlers.onunhandledrejection` mechanism |
| `isTranslateStackOverflow` | Google Translate stack overflow | `RangeError` + "Maximum call stack size exceeded" + `translate_http`/`el_main` frame (`translate.goog` is only the proxy host, not evidence) |
| `isInjectedCookiebotError` | extension collision with Cookiebot | `TypeError: Illegal invocation` + `inject_content` frame + `cc.js`/`uc.js` frame |
| `isNativeBridgeProbeError` | injected `webkit.messageHandlers` probes | `webkit.messageHandlers` message + foreign `app:///` frame + no first-party frame |
| `isJsloaderTrackingScriptError` | injected `tracking_script.js` jsloader failure | `CustomError` + exact jsloader/client.js prefix + `tracking_script.js` frame |

Deliberately **kept reporting** (retain tests pin these):

- `SyntaxError: Invalid or unexpected token` from
`app:///0aa4d9c5efab527d/script.js` — a real Vercel Web Analytics delivery
anomaly we own (#176).
- `ReferenceError: $ is not defined` inside `app:///gtm.js` — our own GTM
container (`GTM-5PFMJFN`), so the tag is ours to fix, not suppress.
Production breadcrumb: `GTM PTag v1.4; tagId: 2613447705545` — `__pntr`
identifies the Pinterest Tag gallery template, so this is a Pinterest tag
configured in the GTM dashboard whose code appears to assume jQuery
(which the site intentionally does not ship). The container is not
versioned in this repo. Owner checklist: in the GTM container, search
tags/templates for the Pinterest template or tag ID `2613447705545`,
inspect its trigger and custom code for `$(`/`jQuery(`, confirm whether
Pinterest tracking is a current business requirement, then either remove
the tag, update the template, or rewrite it with native DOM APIs —
publish only after consent validation. Do not add jQuery or a fake `$`.

## CSP

`connect-src` gains exactly one origin: the `https:` origin parsed out of
Expand Down
155 changes: 153 additions & 2 deletions lib/sentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,147 @@ export function isExtensionSendMessageError(event: ErrorEvent): boolean {
return false;
}

/**
* Google Translate stack overflow: machine-translated pageviews (e.g.
* `www-seasaba-com.translate.goog`) run Google's `translate_http` /
* `el_main` scripts, whose DOM rewriting can recurse until the browser
* throws `RangeError: Maximum call stack size exceeded`. The filter
* requires the overflow message AND a Translate-machinery stack signature
* — an app-side stack overflow, a different RangeError inside Translate
* code, or an overflow in any other vendor still reports. `translate.goog`
* is deliberately not evidence: it is the proxy host on every frame of a
* translated pageview, including our own `/_next/` code.
*/
export function isTranslateStackOverflow(event: ErrorEvent): boolean {
for (const exception of event.exception?.values ?? []) {
const isStackOverflow =
exception.type === "RangeError" &&
typeof exception.value === "string" &&
exception.value.includes("Maximum call stack size exceeded");
if (!isStackOverflow) continue;

if (
stackContains(exception, "translate_http") ||
stackContains(exception, "el_main")
) {
return true;
}
}
return false;
}

/**
* Injected content-script collision with Cookiebot: `TypeError: Illegal
* invocation` thrown inside `app:///dist/inject_content.js` while touching
* Cookiebot's `cc.js`/`uc.js`. `inject_content.js` is a known
* extension/content-script artifact that exists nowhere in our bundle.
* Both foreign frames are required: a Cookiebot defect without the
* injected frame (and any app-side Illegal invocation) still reports.
*/
export function isInjectedCookiebotError(event: ErrorEvent): boolean {
for (const exception of event.exception?.values ?? []) {
const isIllegalInvocation =
exception.type === "TypeError" &&
typeof exception.value === "string" &&
exception.value.includes("Illegal invocation");
if (!isIllegalInvocation) continue;

if (
stackContains(exception, "inject_content") &&
(stackContains(exception, "cc.js") || stackContains(exception, "uc.js"))
) {
return true;
}
}
return false;
}

/**
* Opaque native-bridge probe: `window.webkit.messageHandlers` referenced
* from a foreign `app:///…` script with no first-party frame in the stack.
* The site has no WKWebView bridge; injected scripts probing for one throw
* outside their expected native context. The conjunction keeps this
* narrow: the same message thrown by first-party code (a real future
* native-wrapper integration would run from `/_next/` chunks) or with no
* foreign frame evidence still reports. `gtm.js` frames count as
* first-party presence — the container is ours, so a probe touched by GTM
* tag code keeps reporting like any other own-stack failure.
*/
export function isNativeBridgeProbeError(event: ErrorEvent): boolean {
for (const exception of event.exception?.values ?? []) {
const isBridgeProbe =
typeof exception.value === "string" &&
exception.value.includes("webkit.messageHandlers");
if (!isBridgeProbe) continue;

const frames = exception.stacktrace?.frames ?? [];
const hasForeignScriptFrame = frames.some(
(frame) =>
typeof frame.filename === "string" &&
stripUrlSensitiveParts(frame.filename).startsWith("app:///") &&
!isFirstPartyFrameFilename(frame.filename)
);
const hasFirstPartyFrame = frames.some(
(frame) =>
typeof frame.filename === "string" &&
isFirstPartyFrameFilename(frame.filename)
);
if (hasForeignScriptFrame && !hasFirstPartyFrame) return true;
}
return false;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Google jsloader failure inside an injected `tracking_script.js`: the
* observed event is `CustomError: Jsloader error (code #0): Error while
* loading script https://apis.google.com/js/client.js` with a stack
* entirely in `app:///tracking_script.js` — a foreign tracker script, not
* a repo or deployed-bundle file. The exact error type + exact jsloader
* prefix + the injected frame are all required, so a jsloader failure for
* any other script, the same message from first-party code, or a
* different CustomError still reports.
*/
export function isJsloaderTrackingScriptError(event: ErrorEvent): boolean {
for (const exception of event.exception?.values ?? []) {
const isJsloaderFailure =
exception.type === "CustomError" &&
typeof exception.value === "string" &&
exception.value.startsWith(
"Jsloader error (code #0): Error while loading script https://apis.google.com/js/client.js"
);
if (!isJsloaderFailure) continue;

if (stackContains(exception, "tracking_script.js")) return true;
}
return false;
}

/**
* First-party frame evidence: our compiled chunks (`/_next/` on any host —
* including `translate.goog` proxied pageviews), `webpack://` module paths,
* and the GTM runtime (`gtm.js`; our container is ours to fix, and the URL
* always carries `?id=…`, so compare the sanitized filename).
*/
function isFirstPartyFrameFilename(filename: string): boolean {
const normalized = stripUrlSensitiveParts(filename);
return (
normalized.includes("/_next/") ||
normalized.startsWith("webpack://") ||
normalized.endsWith("/gtm.js")
);
}

/** True when any exception value's stack carries a first-party frame. */
function hasFirstPartyStackFrame(event: ErrorEvent): boolean {
return (event.exception?.values ?? []).some((exception) =>
(exception.stacktrace?.frames ?? []).some(
(frame) =>
typeof frame.filename === "string" &&
isFirstPartyFrameFilename(frame.filename)
)
);
}

/** True when any stack frame's filename or function name contains `needle`. */
function stackContains(
exception: {
Expand All @@ -169,6 +310,10 @@ const KNOWN_FOREIGN_NOISE: ReadonlyArray<(event: ErrorEvent) => boolean> = [
isInjectedMediaFilterError,
isClarityIcuError,
isExtensionSendMessageError,
isTranslateStackOverflow,
isInjectedCookiebotError,
isNativeBridgeProbeError,
isJsloaderTrackingScriptError,
Comment thread
sourcery-ai[bot] marked this conversation as resolved.
];

/**
Expand All @@ -189,8 +334,14 @@ export function sanitizeSentryEvent(event: ErrorEvent): ErrorEvent | null {
// Drop events proven to originate from foreign injected/vendor code (see
// KNOWN_FOREIGN_NOISE — #174 mediafilter, #176 Clarity ICU, #176
// extension runtime.sendMessage). Each rule requires its full signature;
// anything else falls through untouched.
if (KNOWN_FOREIGN_NOISE.some((isForeignNoise) => isForeignNoise(event))) {
// anything else falls through untouched. The first-party veto comes
// first: an event whose exception chain still carries our own frames —
// a real app error alongside matching noise, or our code running inside
// a translated pageview — is never swallowed by a noise rule.
if (
!hasFirstPartyStackFrame(event) &&
KNOWN_FOREIGN_NOISE.some((isForeignNoise) => isForeignNoise(event))
) {
return null;
}

Expand Down
Loading
Loading