Skip to content

Commit 9a5b6bd

Browse files
committed
fix(sentry): repair CSP eval ignore filter that silently never matched
The /unsafe-eval is not an allowed source of script/ regex never matched the real browser message ('unsafe-eval' is … — note the closing quote between the token and 'is'), so the unactionable CSP EvalError kept flowing into Sentry despite the filter shipping weeks ago in 0e70af4. Anchor on the stable, browser-agnostic prefix 'Refused to evaluate a string as JavaScript' instead, and extract all ignoreErrors patterns into a tested src/lib/sentry-filters.ts so a typo'd pattern can't silently let noise through again — the test pins each pattern against the verbatim production message it targets. Also covers the RSC 'Connection closed.' abort and the in-app-WebView 'Unexpected identifier https' injection. Fixes Sentry issue 7551992835.
1 parent 7dcc450 commit 9a5b6bd

3 files changed

Lines changed: 116 additions & 3 deletions

File tree

src/instrumentation-client.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Sentry from '@sentry/nextjs'
2+
import { IGNORE_SENTRY_ERRORS } from '@/lib/sentry-filters'
23

34
Sentry.init({
45
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
@@ -17,9 +18,10 @@ Sentry.init({
1718
replaysSessionSampleRate: 0,
1819
replaysOnErrorSampleRate: 0,
1920

20-
// Browser-extension content scripts that hit our production CSP raise an
21-
// unactionable error here. The CSP is working as intended; drop the noise.
22-
ignoreErrors: [/unsafe-eval is not an allowed source of script/],
21+
// Unactionable browser noise (extensions, in-app WebViews, RSC stream
22+
// aborts). Kept in a tested module so a typo'd pattern can't silently let
23+
// the noise through — see src/lib/sentry-filters.ts.
24+
ignoreErrors: IGNORE_SENTRY_ERRORS,
2325
})
2426

2527
// Instrument App Router navigations

src/lib/sentry-filters.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { IGNORE_SENTRY_ERRORS } from './sentry-filters'
3+
4+
// Mirror of Sentry's own matching (eventFilters.ts / string.ts): an event is
5+
// dropped if ANY ignore pattern matches ANY of its candidate messages — the
6+
// bare exception value and the `Type: value` string. Regex patterns use
7+
// `.test()`, string patterns use substring `.includes()`. Keeping this in sync
8+
// with the SDK is the whole point of the test: it proves each pattern actually
9+
// fires against the real-world message it was written for.
10+
function isIgnored(type: string, value: string): boolean {
11+
const candidates = [value, `${type}: ${value}`]
12+
return candidates.some((message) =>
13+
IGNORE_SENTRY_ERRORS.some((pattern) =>
14+
typeof pattern === 'string'
15+
? message.includes(pattern)
16+
: pattern.test(message),
17+
),
18+
)
19+
}
20+
21+
describe('IGNORE_SENTRY_ERRORS', () => {
22+
it('every entry is a string or RegExp', () => {
23+
expect(IGNORE_SENTRY_ERRORS.length).toBeGreaterThan(0)
24+
for (const pattern of IGNORE_SENTRY_ERRORS) {
25+
expect(['string', 'object']).toContain(typeof pattern)
26+
if (typeof pattern !== 'string') expect(pattern).toBeInstanceOf(RegExp)
27+
}
28+
})
29+
30+
// Real messages seen in production (verbatim values from the browser engines
31+
// that raise them). If a pattern is ever edited into one that no longer
32+
// matches its message — the silent-filter bug this module exists to prevent —
33+
// the corresponding case here fails loudly.
34+
describe('drops unactionable browser noise', () => {
35+
const noise: [name: string, type: string, value: string][] = [
36+
[
37+
'CSP unsafe-eval refusal (browser-extension content script)',
38+
'EvalError',
39+
`Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self'".`,
40+
],
41+
['RSC stream abort', 'Error', 'Connection closed.'],
42+
[
43+
'in-app WebView https injection (issue 7556617342)',
44+
'SyntaxError',
45+
"Unexpected identifier 'https'",
46+
],
47+
]
48+
49+
it.each(noise)('drops: %s', (_name, type, value) => {
50+
expect(isIgnored(type, value)).toBe(true)
51+
})
52+
})
53+
54+
// The filters must stay narrow: a genuine app error — including syntax errors
55+
// that AREN'T the https-injection signature — has to reach Sentry.
56+
describe('keeps genuine, actionable errors', () => {
57+
const real: [name: string, type: string, value: string][] = [
58+
[
59+
'app TypeError',
60+
'TypeError',
61+
"Cannot read properties of undefined (reading 'map')",
62+
],
63+
['old-browser optional chaining in our bundle', 'SyntaxError', "Unexpected token '.'"],
64+
['genuine async syntax error in our bundle', 'SyntaxError', "Unexpected identifier 'await'"],
65+
['network failure', 'Error', 'Failed to fetch'],
66+
['unrelated React error', 'Error', 'Minified React error #418'],
67+
]
68+
69+
it.each(real)('keeps: %s', (_name, type, value) => {
70+
expect(isIgnored(type, value)).toBe(false)
71+
})
72+
})
73+
})

src/lib/sentry-filters.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// Client-side Sentry `ignoreErrors` patterns.
2+
//
3+
// Each entry drops a class of *unactionable* browser noise — errors raised by
4+
// browser extensions, in-app WebViews, content-injecting proxies, or RSC
5+
// stream aborts, none of which our application code can fix. Kept here as pure
6+
// data (rather than inline in `instrumentation-client.ts`) so the patterns can
7+
// be unit-tested: a regex that silently never matches its real-world message
8+
// is worse than no filter at all, because the noise keeps flowing while
9+
// looking handled. See `sentry-filters.test.ts`.
10+
//
11+
// Sentry matches these against the exception value, the `Type: value` string,
12+
// and `event.message`: regex entries via `.test()`, string entries via
13+
// substring `.includes()`. Anchor regexes on the STABLE prefix of a message,
14+
// never on a fragment that depends on quoting the browser may render
15+
// differently across engines.
16+
export const IGNORE_SENTRY_ERRORS: (string | RegExp)[] = [
17+
// Browser-extension content scripts that hit our production CSP raise an
18+
// unactionable EvalError here. The CSP is working as intended; drop the
19+
// noise. Anchor on the stable message prefix — the real message reads
20+
// `…because 'unsafe-eval' is not an allowed source…`, so a regex expecting
21+
// `unsafe-eval is` (no closing quote) silently never matched.
22+
/Refused to evaluate a string as JavaScript/,
23+
// React Server Components stream abort. React's flight client throws this
24+
// when the RSC payload connection closes before it finishes reading —
25+
// overwhelmingly caused by the user navigating away (or a prefetch being
26+
// cancelled, or a bot closing the socket early) mid-stream, not by app
27+
// code. A genuine server-side streaming failure surfaces separately in the
28+
// server-side Sentry instrumentation, so dropping it here only removes noise.
29+
'Connection closed.',
30+
// In-app browsers, old Android WebViews, and content-injecting
31+
// proxies/extensions splice a mangled script into the page — a raw
32+
// `https://…` URL pasted into a script context parses as a bare `https`
33+
// identifier and throws this SyntaxError. The global onerror handler
34+
// attributes it to the document URL at line 1 with no chunk file in the
35+
// stack, confirming it isn't our bundle (which never emits an unquoted
36+
// `https` token), so this only drops third-party injection noise.
37+
/Unexpected identifier 'https'/,
38+
]

0 commit comments

Comments
 (0)