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
125 changes: 125 additions & 0 deletions packages/worker/client/sentry-browser-filters.node.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
filterBrowserAbortSentryEvent,
filterBrowserInjectedGlobalNoiseSentryEvent,
filterBrowserSentryEvent,
filterFathomRemoveChildNullSentryEvent,
filterFirefoxDomPermissionDeniedSentryEvent,
} from './sentry-browser-filters.ts'

Expand Down Expand Up @@ -244,4 +245,128 @@ test('browser Sentry filters drop AbortError and Firefox Xray noise and keep rea
},
}),
).toBeNull()

// Fathom beacon removeChild-on-null only when a usefathom.com frame is present
// (issue 7653117289 / KODY-CLOUDFLARE-3Q).
expect(
filterFathomRemoveChildNullSentryEvent({
exception: {
values: [
{
type: 'TypeError',
value: "Cannot read properties of null (reading 'removeChild')",
stacktrace: {
frames: [
{
filename: '/script.js',
abs_path: 'https://cdn.usefathom.com/script.js',
function: 'HTMLImageElement.<anonymous>',
},
],
},
},
],
},
}),
).toBeNull()
expect(
filterFathomRemoveChildNullSentryEvent({
exception: {
values: [
{
type: 'TypeError',
value:
"TypeError: Cannot read properties of null (reading 'removeChild')",
stacktrace: {
frames: [
{
filename: 'https://cdn.usefathom.com/script.js',
},
],
},
},
],
},
}),
).toBeNull()
// Same message without a Fathom frame must stay (could be app code).
expect(
filterFathomRemoveChildNullSentryEvent({
exception: {
values: [
{
type: 'TypeError',
value: "Cannot read properties of null (reading 'removeChild')",
stacktrace: {
frames: [
{
filename: '/assets/app-chunk.js',
abs_path: 'https://heykody.dev/assets/app-chunk.js',
},
],
},
},
],
},
}),
).not.toBeNull()
// Pathname/substring lookalike on a non-Fathom host must stay.
expect(
filterFathomRemoveChildNullSentryEvent({
exception: {
values: [
{
type: 'TypeError',
value: "Cannot read properties of null (reading 'removeChild')",
stacktrace: {
frames: [
{
abs_path: 'https://heykody.dev/cdn.usefathom.com/script.js',
},
],
},
},
],
},
}),
).not.toBeNull()
// Fathom frame with a different error must stay.
expect(
filterFathomRemoveChildNullSentryEvent({
exception: {
values: [
{
type: 'TypeError',
value: "Cannot read properties of null (reading 'foo')",
stacktrace: {
frames: [
{
abs_path: 'https://cdn.usefathom.com/script.js',
},
],
},
},
],
},
}),
).not.toBeNull()
expect(
filterBrowserSentryEvent({
exception: {
values: [
{
type: 'TypeError',
value: "Cannot read properties of null (reading 'removeChild')",
stacktrace: {
frames: [
{
abs_path: 'https://cdn.usefathom.com/script.js',
},
],
},
},
],
},
}),
).toBeNull()
})
75 changes: 75 additions & 0 deletions packages/worker/client/sentry-browser-filters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
* tests can exercise the predicates with plain event shapes.
*/

type SentryStackFrame = {
filename?: string
abs_path?: string
absPath?: string
}

type SentryExceptionValue = {
type?: string
value?: string
stacktrace?: {
frames?: Array<SentryStackFrame>
}
}

export type SentryErrorEventLike = {
Expand All @@ -22,6 +31,20 @@ function sentryEventMessages(event: SentryErrorEventLike) {
]
}

function sentryEventStackFrameUrls(event: SentryErrorEventLike) {
const urls: Array<string> = []
for (const value of event.exception?.values ?? []) {
for (const frame of value.stacktrace?.frames ?? []) {
for (const candidate of [frame.abs_path, frame.absPath, frame.filename]) {
if (typeof candidate === 'string' && candidate.length > 0) {
urls.push(candidate)
}
}
}
}
return urls
}

/**
* Chromium/Edge fetch abort text from KODY-CLOUDFLARE-23 and common browser
* variants. Matching is intentionally narrow: only these exact abort strings
Expand Down Expand Up @@ -181,6 +204,55 @@ export function filterBrowserInjectedGlobalNoiseSentryEvent<
return event
}

/**
* Fathom Analytics (`cdn.usefathom.com/script.js`) tracks pageviews with a
* temporary `<img>` beacon and removes it from `load`/`error` handlers via
* `img.parentNode.removeChild(img)`. When the beacon was already detached
* (SPA navigation / soft reload), `parentNode` is null and Chromium throws.
* Signature from production issue 7653117289 / KODY-CLOUDFLARE-3Q.
*
* Match is intentionally narrow: removeChild-on-null TypeError text AND a
* stack frame from the Fathom CDN. Never blanket-drop removeChild errors from
* app code.
*/
const fathomRemoveChildNullMessage =
/^(?:TypeError:\s*)?Cannot read propert(?:y|ies) of null \(reading ['"]removeChild['"]\)$/

const fathomAnalyticsHostname = 'cdn.usefathom.com'

export function isFathomRemoveChildNullMessage(message: string) {
return fathomRemoveChildNullMessage.test(message.trim())
}

export function isFathomAnalyticsStackFrameUrl(url: string) {
try {
return (
new URL(url, 'https://sentry.invalid').hostname ===
fathomAnalyticsHostname
)
} catch {
return false
}
}

export function isFathomRemoveChildNullSentryEvent(
event: SentryErrorEventLike,
) {
const hasRemoveChildNull = sentryEventMessages(event).some(
(message) =>
typeof message === 'string' && isFathomRemoveChildNullMessage(message),
)
if (!hasRemoveChildNull) return false
return sentryEventStackFrameUrls(event).some(isFathomAnalyticsStackFrameUrl)
}

export function filterFathomRemoveChildNullSentryEvent<
T extends SentryErrorEventLike,
>(event: T): T | null {
if (isFathomRemoveChildNullSentryEvent(event)) return null
return event
}

/** Combined browser beforeSend / capture gate used by the client SDK. */
export function filterBrowserSentryEvent<T extends SentryErrorEventLike>(
event: T,
Expand All @@ -201,5 +273,8 @@ export function filterBrowserSentryEvent<T extends SentryErrorEventLike>(
) {
return null
}
if (filterFathomRemoveChildNullSentryEvent(event) === null) {
return null
}
return event
}
8 changes: 5 additions & 3 deletions packages/worker/client/sentry-init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,11 @@ export function initBrowserSentry(config: SentryClientConfig) {
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 1.0,
// Drop expected browser noise (AbortError aborts, Firefox DOM
// permission-denied from Replay/hydration on restricted nodes, and
// injected wallet/`__firefox__` globals) — see filterBrowserSentryEvent
// / KODY-CLOUDFLARE-23 / issues 7639685398, 7648833360, 7648833403.
// permission-denied from Replay/hydration on restricted nodes,
// injected wallet/`__firefox__` globals, and Fathom beacon
// removeChild-on-null) — see filterBrowserSentryEvent /
// KODY-CLOUDFLARE-23 / KODY-CLOUDFLARE-3Q / issues 7639685398,
// 7648833360, 7648833403, 7653117289.
beforeSend(event, hint) {
return filterBrowserSentryEvent(event, hint.originalException)
},
Expand Down
Loading