Skip to content

Commit 1832e16

Browse files
committed
fix: ignore safari autofill sentry noise
1 parent c40aca8 commit 1832e16

2 files changed

Lines changed: 93 additions & 5 deletions

File tree

frontend/src/lib/sentry.test.ts

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,12 @@ vi.mock('@sentry/react', () => ({
1010
setUser: sentrySetUserMock,
1111
}));
1212

13-
import { getFrontendSentryConfig, initFrontendSentry, setSentryUser } from './sentry';
13+
import {
14+
getFrontendSentryConfig,
15+
initFrontendSentry,
16+
setSentryUser,
17+
shouldIgnoreThirdPartySafariAutofillError,
18+
} from './sentry';
1419

1520
describe('frontend sentry config', () => {
1621
beforeEach(() => {
@@ -37,12 +42,13 @@ describe('frontend sentry config', () => {
3742
VITE_SENTRY_RELEASE: 'awork-forms@1.2.3',
3843
});
3944

40-
expect(sentryInitMock).toHaveBeenCalledWith({
45+
expect(sentryInitMock).toHaveBeenCalledWith(expect.objectContaining({
4146
dsn: 'https://key@o0.ingest.sentry.io/1',
4247
environment: 'staging',
4348
release: 'awork-forms@1.2.3',
4449
tracesSampleRate: 1.0,
45-
});
50+
beforeSend: expect.any(Function),
51+
}));
4652
});
4753

4854
it('does not initialize sentry when dsn is blank', async () => {
@@ -75,12 +81,57 @@ describe('frontend sentry config', () => {
7581
VITE_SENTRY_RELEASE: 'vite-release',
7682
});
7783

78-
expect(sentryInitMock).toHaveBeenCalledWith({
84+
expect(sentryInitMock).toHaveBeenCalledWith(expect.objectContaining({
7985
dsn: 'https://runtime@o0.ingest.sentry.io/1',
8086
environment: 'runtime-env',
8187
release: 'runtime-release',
8288
tracesSampleRate: 1.0,
83-
});
89+
beforeSend: expect.any(Function),
90+
}));
91+
});
92+
93+
it('ignores Safari autofill errors from masked third-party frames', () => {
94+
const event = {
95+
type: undefined,
96+
exception: {
97+
values: [
98+
{
99+
type: 'TypeError',
100+
value: "null is not an object (evaluating 'autofillFieldData.autoCompleteType.includes')",
101+
stacktrace: {
102+
frames: [
103+
{ filename: 'webkit-masked-url://hidden/', function: 'setupOverlayOnField' },
104+
{ filename: '[native code]', function: 'Promise' },
105+
],
106+
},
107+
},
108+
],
109+
},
110+
} satisfies Parameters<typeof shouldIgnoreThirdPartySafariAutofillError>[0];
111+
112+
expect(shouldIgnoreThirdPartySafariAutofillError(event)).toBe(true);
113+
});
114+
115+
it('keeps application errors with real app frames', () => {
116+
const event = {
117+
type: undefined,
118+
exception: {
119+
values: [
120+
{
121+
type: 'TypeError',
122+
value: "Cannot read properties of undefined (reading 'id')",
123+
stacktrace: {
124+
frames: [
125+
{ filename: 'https://forms.awork.com/assets/index.js', function: 'renderField' },
126+
{ filename: 'https://forms.awork.com/assets/index.js', function: 'handleSave' },
127+
],
128+
},
129+
},
130+
],
131+
},
132+
} satisfies Parameters<typeof shouldIgnoreThirdPartySafariAutofillError>[0];
133+
134+
expect(shouldIgnoreThirdPartySafariAutofillError(event)).toBe(false);
84135
});
85136

86137
it('sets sentry user with id, email, and workspace_id', () => {

frontend/src/lib/sentry.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import * as Sentry from '@sentry/react';
2+
import type { ErrorEvent, Exception, StackFrame } from '@sentry/react';
23

34
export interface FrontendSentryEnv {
45
MODE?: string;
@@ -20,6 +21,35 @@ export interface FrontendSentryConfig {
2021
release?: string;
2122
}
2223

24+
const SAFARI_MASKED_URL_PREFIX = 'webkit-masked-url://hidden/';
25+
26+
function getExceptionFrames(exception: Exception): StackFrame[] {
27+
return exception.stacktrace?.frames || [];
28+
}
29+
30+
function hasOnlySafariMaskedFrames(exception: Exception) {
31+
const frames = getExceptionFrames(exception);
32+
return frames.length > 0 && frames.every((frame) => {
33+
const filename = frame.filename || frame.abs_path;
34+
return filename === '[native code]' || filename?.startsWith(SAFARI_MASKED_URL_PREFIX);
35+
});
36+
}
37+
38+
export function shouldIgnoreThirdPartySafariAutofillError(event: ErrorEvent): boolean {
39+
const exceptions = event.exception?.values || [];
40+
if (exceptions.length === 0) return false;
41+
42+
const combinedMessage = exceptions
43+
.map((exception) => `${exception.type || ''} ${exception.value || ''}`.toLowerCase())
44+
.join('\n');
45+
46+
const isKnownSafariAutofillFailure = combinedMessage.includes('autofillfielddata')
47+
|| combinedMessage.includes('autocompletetype.includes');
48+
49+
return isKnownSafariAutofillFailure
50+
&& exceptions.every((exception) => hasOnlySafariMaskedFrames(exception));
51+
}
52+
2353
export async function fetchRuntimeSentryConfig(fetchImpl: typeof fetch = fetch): Promise<RuntimeSentryConfig | undefined> {
2454
try {
2555
const response = await fetchImpl('/api/app-config', {
@@ -66,5 +96,12 @@ export async function initFrontendSentry(env: FrontendSentryEnv = import.meta.en
6696
environment: config.environment,
6797
release: config.release,
6898
tracesSampleRate: 1.0,
99+
beforeSend(event) {
100+
if (shouldIgnoreThirdPartySafariAutofillError(event)) {
101+
return null;
102+
}
103+
104+
return event;
105+
},
69106
});
70107
}

0 commit comments

Comments
 (0)