Skip to content

Commit 862a096

Browse files
committed
fix(browser): Forward worker metadata for third-party error filtering
The `thirdPartyErrorFilterIntegration` was not able to identify first-party worker code as because module metadata stayed in the worker's separate global scope and wasn't accessible to the main thread. We now forward the metadata the same way we forward debug ids to the main thread which allows first-party worker code to be identified as such. Closes: #18705
1 parent 5901e70 commit 862a096

File tree

8 files changed

+402
-8
lines changed

8 files changed

+402
-8
lines changed

dev-packages/e2e-tests/test-applications/browser-webworker-vite/src/main.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ Sentry.init({
77
environment: import.meta.env.MODE || 'development',
88
tracesSampleRate: 1.0,
99
debug: true,
10-
integrations: [Sentry.browserTracingIntegration()],
10+
integrations: [
11+
Sentry.browserTracingIntegration(),
12+
Sentry.thirdPartyErrorFilterIntegration({
13+
behaviour: 'apply-tag-if-contains-third-party-frames',
14+
filterKeys: ['browser-webworker-vite'],
15+
}),
16+
],
1117
tunnel: 'http://localhost:3031/', // proxy server
1218
});
1319

dev-packages/e2e-tests/test-applications/browser-webworker-vite/tests/errors.test.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,3 +171,19 @@ test('captures an error from the third lazily added worker', async ({ page }) =>
171171
],
172172
});
173173
});
174+
175+
test('worker errors are not tagged as third-party when module metadata is present', async ({ page }) => {
176+
const errorEventPromise = waitForError('browser-webworker-vite', async event => {
177+
return !event.type && event.exception?.values?.[0]?.value === 'Uncaught Error: Uncaught error in worker';
178+
});
179+
180+
await page.goto('/');
181+
182+
await page.locator('#trigger-error').click();
183+
184+
await page.waitForTimeout(1000);
185+
186+
const errorEvent = await errorEventPromise;
187+
188+
expect(errorEvent.tags?.third_party_code).toBeUndefined();
189+
});

dev-packages/e2e-tests/test-applications/browser-webworker-vite/vite.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export default defineConfig({
1212
org: process.env.E2E_TEST_SENTRY_ORG_SLUG,
1313
project: process.env.E2E_TEST_SENTRY_PROJECT,
1414
authToken: process.env.E2E_TEST_AUTH_TOKEN,
15+
applicationKey: 'browser-webworker-vite',
1516
}),
1617
],
1718

@@ -21,6 +22,7 @@ export default defineConfig({
2122
org: process.env.E2E_TEST_SENTRY_ORG_SLUG,
2223
project: process.env.E2E_TEST_SENTRY_PROJECT,
2324
authToken: process.env.E2E_TEST_AUTH_TOKEN,
25+
applicationKey: 'browser-webworker-vite',
2426
}),
2527
],
2628
},

packages/browser/src/integrations/webWorker.ts

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
11
import type { Integration, IntegrationFn } from '@sentry/core';
2-
import { captureEvent, debug, defineIntegration, getClient, isPlainObject, isPrimitive } from '@sentry/core';
2+
import {
3+
captureEvent,
4+
debug,
5+
defineIntegration,
6+
getClient,
7+
getFilenameToMetadataMap,
8+
isPlainObject,
9+
isPrimitive,
10+
mergeMetadataMap,
11+
} from '@sentry/core';
312
import { DEBUG_BUILD } from '../debug-build';
413
import { eventFromUnknownInput } from '../eventbuilder';
514
import { WINDOW } from '../helpers';
15+
import { defaultStackParser } from '../stack-parsers';
616
import { _eventFromRejectionWithPrimitive, _getUnhandledRejectionError } from './globalhandlers';
717

818
export const INTEGRATION_NAME = 'WebWorker';
919

1020
interface WebWorkerMessage {
1121
_sentryMessage: boolean;
1222
_sentryDebugIds?: Record<string, string>;
23+
_sentryModuleMetadata?: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
1324
_sentryWorkerError?: SerializedWorkerError;
1425
}
1526

@@ -122,6 +133,13 @@ function listenForSentryMessages(worker: Worker): void {
122133
};
123134
}
124135

136+
// Handle module metadata
137+
if (event.data._sentryModuleMetadata) {
138+
DEBUG_BUILD && debug.log('Sentry module metadata web worker message received', event.data);
139+
// Merge worker metadata into the main thread's metadata cache
140+
mergeMetadataMap(event.data._sentryModuleMetadata);
141+
}
142+
125143
// Handle unhandled rejections forwarded from worker
126144
if (event.data._sentryWorkerError) {
127145
DEBUG_BUILD && debug.log('Sentry worker rejection message received', event.data._sentryWorkerError);
@@ -187,14 +205,18 @@ interface MinimalDedicatedWorkerGlobalScope {
187205
}
188206

189207
interface RegisterWebWorkerOptions {
190-
self: MinimalDedicatedWorkerGlobalScope & { _sentryDebugIds?: Record<string, string> };
208+
self: MinimalDedicatedWorkerGlobalScope & {
209+
_sentryDebugIds?: Record<string, string>;
210+
_sentryModuleMetadata?: Record<string, any>; // eslint-disable-line @typescript-eslint/no-explicit-any
211+
};
191212
}
192213

193214
/**
194215
* Use this function to register the worker with the Sentry SDK.
195216
*
196217
* This function will:
197218
* - Send debug IDs to the parent thread
219+
* - Send module metadata to the parent thread (for thirdPartyErrorFilterIntegration)
198220
* - Set up a handler for unhandled rejections in the worker
199221
* - Forward unhandled rejections to the parent thread for capture
200222
*
@@ -215,10 +237,13 @@ interface RegisterWebWorkerOptions {
215237
* - `self`: The worker instance you're calling this function from (self).
216238
*/
217239
export function registerWebWorker({ self }: RegisterWebWorkerOptions): void {
218-
// Send debug IDs to parent thread
240+
const moduleMetadata = self._sentryModuleMetadata ? getFilenameToMetadataMap(defaultStackParser) : undefined;
241+
242+
// Send debug IDs and module metadata to parent thread
219243
self.postMessage({
220244
_sentryMessage: true,
221245
_sentryDebugIds: self._sentryDebugIds ?? undefined,
246+
_sentryModuleMetadata: moduleMetadata,
222247
});
223248

224249
// Set up unhandledrejection handler inside the worker
@@ -251,11 +276,12 @@ function isSentryMessage(eventData: unknown): eventData is WebWorkerMessage {
251276
return false;
252277
}
253278

254-
// Must have at least one of: debug IDs or worker error
279+
// Must have at least one of: debug IDs, module metadata, or worker error
255280
const hasDebugIds = '_sentryDebugIds' in eventData;
281+
const hasModuleMetadata = '_sentryModuleMetadata' in eventData;
256282
const hasWorkerError = '_sentryWorkerError' in eventData;
257283

258-
if (!hasDebugIds && !hasWorkerError) {
284+
if (!hasDebugIds && !hasModuleMetadata && !hasWorkerError) {
259285
return false;
260286
}
261287

@@ -264,6 +290,14 @@ function isSentryMessage(eventData: unknown): eventData is WebWorkerMessage {
264290
return false;
265291
}
266292

293+
// Validate module metadata if present
294+
if (
295+
hasModuleMetadata &&
296+
!(isPlainObject(eventData._sentryModuleMetadata) || eventData._sentryModuleMetadata === undefined)
297+
) {
298+
return false;
299+
}
300+
267301
// Validate worker error if present
268302
if (hasWorkerError && !isPlainObject(eventData._sentryWorkerError)) {
269303
return false;

packages/browser/test/integrations/webWorker.test.ts

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ vi.mock('@sentry/core', async importActual => {
1414
debug: {
1515
log: vi.fn(),
1616
},
17+
mergeMetadataMap: vi.fn(),
18+
getFilenameToMetadataMap: vi.fn(),
1719
};
1820
});
1921

@@ -209,6 +211,74 @@ describe('webWorkerIntegration', () => {
209211
'main.js': 'main-debug',
210212
});
211213
});
214+
215+
it('processes module metadata from worker', () => {
216+
const mockMergeMetadataMap = SentryCore.mergeMetadataMap as any;
217+
const moduleMetadata = {
218+
'worker-file1.js': { '_sentryBundlerPluginAppKey:my-app': true },
219+
'worker-file2.js': { '_sentryBundlerPluginAppKey:my-app': true },
220+
};
221+
222+
mockEvent.data = {
223+
_sentryMessage: true,
224+
_sentryModuleMetadata: moduleMetadata,
225+
};
226+
227+
messageHandler(mockEvent);
228+
229+
expect(mockEvent.stopImmediatePropagation).toHaveBeenCalled();
230+
expect(mockDebugLog).toHaveBeenCalledWith('Sentry module metadata web worker message received', mockEvent.data);
231+
expect(mockMergeMetadataMap).toHaveBeenCalledWith(moduleMetadata);
232+
});
233+
234+
it('handles message with both debug IDs and module metadata', () => {
235+
const mockMergeMetadataMap = SentryCore.mergeMetadataMap as any;
236+
const moduleMetadata = {
237+
'worker-file.js': { '_sentryBundlerPluginAppKey:my-app': true },
238+
};
239+
240+
mockEvent.data = {
241+
_sentryMessage: true,
242+
_sentryDebugIds: { 'worker-file.js': 'debug-id-1' },
243+
_sentryModuleMetadata: moduleMetadata,
244+
};
245+
246+
messageHandler(mockEvent);
247+
248+
expect(mockEvent.stopImmediatePropagation).toHaveBeenCalled();
249+
expect(mockMergeMetadataMap).toHaveBeenCalledWith(moduleMetadata);
250+
expect((helpers.WINDOW as any)._sentryDebugIds).toEqual({
251+
'worker-file.js': 'debug-id-1',
252+
});
253+
});
254+
255+
it('accepts message with only module metadata', () => {
256+
const mockMergeMetadataMap = SentryCore.mergeMetadataMap as any;
257+
const moduleMetadata = {
258+
'worker-file.js': { '_sentryBundlerPluginAppKey:my-app': true },
259+
};
260+
261+
mockEvent.data = {
262+
_sentryMessage: true,
263+
_sentryModuleMetadata: moduleMetadata,
264+
};
265+
266+
messageHandler(mockEvent);
267+
268+
expect(mockEvent.stopImmediatePropagation).toHaveBeenCalled();
269+
expect(mockMergeMetadataMap).toHaveBeenCalledWith(moduleMetadata);
270+
});
271+
272+
it('ignores invalid module metadata', () => {
273+
mockEvent.data = {
274+
_sentryMessage: true,
275+
_sentryModuleMetadata: 'not-an-object',
276+
};
277+
278+
messageHandler(mockEvent);
279+
280+
expect(mockEvent.stopImmediatePropagation).not.toHaveBeenCalled();
281+
});
212282
});
213283
});
214284
});
@@ -218,6 +288,7 @@ describe('registerWebWorker', () => {
218288
postMessage: ReturnType<typeof vi.fn>;
219289
addEventListener: ReturnType<typeof vi.fn>;
220290
_sentryDebugIds?: Record<string, string>;
291+
_sentryModuleMetadata?: Record<string, any>;
221292
};
222293

223294
beforeEach(() => {
@@ -236,6 +307,7 @@ describe('registerWebWorker', () => {
236307
expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({
237308
_sentryMessage: true,
238309
_sentryDebugIds: undefined,
310+
_sentryModuleMetadata: undefined,
239311
});
240312
});
241313

@@ -254,6 +326,7 @@ describe('registerWebWorker', () => {
254326
'worker-file1.js': 'debug-id-1',
255327
'worker-file2.js': 'debug-id-2',
256328
},
329+
_sentryModuleMetadata: undefined,
257330
});
258331
});
259332

@@ -266,6 +339,72 @@ describe('registerWebWorker', () => {
266339
expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({
267340
_sentryMessage: true,
268341
_sentryDebugIds: undefined,
342+
_sentryModuleMetadata: undefined,
343+
});
344+
});
345+
346+
it('calls getFilenameToMetadataMap when module metadata is available', () => {
347+
const mockGetFilenameToMetadataMap = SentryCore.getFilenameToMetadataMap as any;
348+
const extractedMetadata = {
349+
'worker-file1.js': { '_sentryBundlerPluginAppKey:my-app': true },
350+
'worker-file2.js': { '_sentryBundlerPluginAppKey:my-app': true },
351+
};
352+
353+
mockWorkerSelf._sentryModuleMetadata = {
354+
'Error\n at worker-file1.js:1:1': { '_sentryBundlerPluginAppKey:my-app': true },
355+
'Error\n at worker-file2.js:1:1': { '_sentryBundlerPluginAppKey:my-app': true },
356+
};
357+
358+
mockGetFilenameToMetadataMap.mockReturnValue(extractedMetadata);
359+
360+
registerWebWorker({ self: mockWorkerSelf as any });
361+
362+
expect(mockGetFilenameToMetadataMap).toHaveBeenCalledWith(expect.any(Function));
363+
expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({
364+
_sentryMessage: true,
365+
_sentryDebugIds: undefined,
366+
_sentryModuleMetadata: extractedMetadata,
367+
});
368+
});
369+
370+
it('does not call getFilenameToMetadataMap when module metadata is not available', () => {
371+
const mockGetFilenameToMetadataMap = SentryCore.getFilenameToMetadataMap as any;
372+
373+
mockWorkerSelf._sentryModuleMetadata = undefined;
374+
375+
registerWebWorker({ self: mockWorkerSelf as any });
376+
377+
expect(mockGetFilenameToMetadataMap).not.toHaveBeenCalled();
378+
expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({
379+
_sentryMessage: true,
380+
_sentryDebugIds: undefined,
381+
_sentryModuleMetadata: undefined,
382+
});
383+
});
384+
385+
it('includes both debug IDs and module metadata when both available', () => {
386+
const mockGetFilenameToMetadataMap = SentryCore.getFilenameToMetadataMap as any;
387+
const extractedMetadata = {
388+
'worker-file.js': { '_sentryBundlerPluginAppKey:my-app': true },
389+
};
390+
391+
mockWorkerSelf._sentryDebugIds = {
392+
'worker-file.js': 'debug-id-1',
393+
};
394+
mockWorkerSelf._sentryModuleMetadata = {
395+
'Error\n at worker-file.js:1:1': { '_sentryBundlerPluginAppKey:my-app': true },
396+
};
397+
398+
mockGetFilenameToMetadataMap.mockReturnValue(extractedMetadata);
399+
400+
registerWebWorker({ self: mockWorkerSelf as any });
401+
402+
expect(mockWorkerSelf.postMessage).toHaveBeenCalledWith({
403+
_sentryMessage: true,
404+
_sentryDebugIds: {
405+
'worker-file.js': 'debug-id-1',
406+
},
407+
_sentryModuleMetadata: extractedMetadata,
269408
});
270409
});
271410
});

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ export { vercelWaitUntil } from './utils/vercelWaitUntil';
322322
export { flushIfServerless } from './utils/flushIfServerless';
323323
export { SDK_VERSION } from './utils/version';
324324
export { getDebugImagesForResources, getFilenameToDebugIdMap } from './utils/debug-ids';
325+
export { getFilenameToMetadataMap, mergeMetadataMap } from './metadata';
325326
export { escapeStringForRegex } from './vendor/escapeStringForRegex';
326327

327328
export type { Attachment } from './types-hoist/attachment';

0 commit comments

Comments
 (0)