Skip to content

Commit fd6cec8

Browse files
committed
support multiple workers
1 parent 9ff304c commit fd6cec8

File tree

7 files changed

+291
-24
lines changed

7 files changed

+291
-24
lines changed

dev-packages/e2e-tests/test-applications/browser-webworker-vite/index.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,11 @@
1111
<button id="trigger-error" type="button" style="background-color: #dc3545; color: white">
1212
Trigger Worker Error
1313
</button>
14+
<button id="trigger-error-2" type="button" style="background-color: #dc3545; color: white">
15+
Trigger Worker 2 Error
16+
</button>
17+
<button id="trigger-error-3" type="button" style="background-color: #dc3545; color: white">
18+
Trigger Worker 3 (lazily added) Error
19+
</button>
1420
</body>
1521
</html>

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import MyWorker from './worker.ts?worker';
2+
import MyWorker2 from './worker2.ts?worker';
23
import * as Sentry from '@sentry/browser';
34

45
Sentry.init({
@@ -11,8 +12,10 @@ Sentry.init({
1112
});
1213

1314
const worker = new MyWorker();
15+
const worker2 = new MyWorker2();
1416

15-
Sentry.addIntegration(Sentry.webWorkerIntegration({ worker }));
17+
const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: [worker, worker2] });
18+
Sentry.addIntegration(webWorkerIntegration);
1619

1720
worker.addEventListener('message', event => {
1821
// this is part of the test, do not delete
@@ -24,3 +27,18 @@ document.querySelector<HTMLButtonElement>('#trigger-error')!.addEventListener('c
2427
msg: 'TRIGGER_ERROR',
2528
});
2629
});
30+
31+
document.querySelector<HTMLButtonElement>('#trigger-error-2')!.addEventListener('click', () => {
32+
worker2.postMessage({
33+
msg: 'TRIGGER_ERROR',
34+
});
35+
});
36+
37+
document.querySelector<HTMLButtonElement>('#trigger-error-3')!.addEventListener('click', async () => {
38+
const Worker3 = await import('./worker3.ts?worker');
39+
const worker3 = new Worker3.default();
40+
webWorkerIntegration.addWorker(worker3);
41+
worker3.postMessage({
42+
msg: 'TRIGGER_ERROR',
43+
});
44+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
// type cast necessary because TS thinks this file is part of the main
4+
// thread where self is of type `Window` instead of `Worker`
5+
Sentry.registerWebWorker({ self: self as unknown as Worker });
6+
7+
// Let the main thread know the worker is ready
8+
self.postMessage({
9+
msg: 'WORKER_2_READY',
10+
});
11+
12+
self.addEventListener('message', event => {
13+
if (event.data.msg === 'TRIGGER_ERROR') {
14+
// This will throw an uncaught error in the worker
15+
throw new Error(`Uncaught error in worker 2`);
16+
}
17+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
// type cast necessary because TS thinks this file is part of the main
4+
// thread where self is of type `Window` instead of `Worker`
5+
Sentry.registerWebWorker({ self: self as unknown as Worker });
6+
7+
// Let the main thread know the worker is ready
8+
self.postMessage({
9+
msg: 'WORKER_3_READY',
10+
});
11+
12+
self.addEventListener('message', event => {
13+
if (event.data.msg === 'TRIGGER_ERROR') {
14+
// This will throw an uncaught error in the worker
15+
throw new Error(`Uncaught error in worker 3`);
16+
}
17+
});

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

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,103 @@ test("user worker message handlers don't trigger for sentry messages", async ({
7171

7272
expect(workerMessageCount).toBe(1);
7373
});
74+
75+
test('captures an error from the second eagerly added worker', async ({ page }) => {
76+
const errorEventPromise = waitForError('browser-webworker-vite', async event => {
77+
return !event.type && !!event.exception?.values?.[0];
78+
});
79+
80+
const transactionPromise = waitForTransaction('browser-webworker-vite', transactionEvent => {
81+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
82+
});
83+
84+
await page.goto('/');
85+
86+
await page.locator('#trigger-error-2').click();
87+
88+
await page.waitForTimeout(1000);
89+
90+
const errorEvent = await errorEventPromise;
91+
const transactionEvent = await transactionPromise;
92+
93+
const pageloadTraceId = transactionEvent.contexts?.trace?.trace_id;
94+
const pageloadSpanId = transactionEvent.contexts?.trace?.span_id;
95+
96+
expect(errorEvent.exception?.values).toHaveLength(1);
97+
expect(errorEvent.exception?.values?.[0]?.value).toBe('Uncaught Error: Uncaught error in worker 2');
98+
expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toHaveLength(1);
99+
expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames?.[0]?.filename).toMatch(/worker2-.+\.js$/);
100+
101+
expect(errorEvent.transaction).toBe('/');
102+
expect(transactionEvent.transaction).toBe('/');
103+
104+
expect(errorEvent.request).toEqual({
105+
url: 'http://localhost:3030/',
106+
headers: expect.any(Object),
107+
});
108+
109+
expect(errorEvent.contexts?.trace).toEqual({
110+
trace_id: pageloadTraceId,
111+
span_id: pageloadSpanId,
112+
});
113+
114+
expect(errorEvent.debug_meta).toEqual({
115+
images: [
116+
{
117+
code_file: expect.stringMatching(/http:\/\/localhost:3030\/assets\/worker2-.+\.js/),
118+
debug_id: expect.stringMatching(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/),
119+
type: 'sourcemap',
120+
},
121+
],
122+
});
123+
});
124+
125+
test('captures an error from the third lazily added worker', async ({ page }) => {
126+
const errorEventPromise = waitForError('browser-webworker-vite', async event => {
127+
return !event.type && !!event.exception?.values?.[0];
128+
});
129+
130+
const transactionPromise = waitForTransaction('browser-webworker-vite', transactionEvent => {
131+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
132+
});
133+
134+
await page.goto('/');
135+
136+
await page.locator('#trigger-error-3').click();
137+
138+
await page.waitForTimeout(1000);
139+
140+
const errorEvent = await errorEventPromise;
141+
const transactionEvent = await transactionPromise;
142+
143+
const pageloadTraceId = transactionEvent.contexts?.trace?.trace_id;
144+
const pageloadSpanId = transactionEvent.contexts?.trace?.span_id;
145+
146+
expect(errorEvent.exception?.values).toHaveLength(1);
147+
expect(errorEvent.exception?.values?.[0]?.value).toBe('Uncaught Error: Uncaught error in worker 3');
148+
expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames).toHaveLength(1);
149+
expect(errorEvent.exception?.values?.[0]?.stacktrace?.frames?.[0]?.filename).toMatch(/worker3-.+\.js$/);
150+
151+
expect(errorEvent.transaction).toBe('/');
152+
expect(transactionEvent.transaction).toBe('/');
153+
154+
expect(errorEvent.request).toEqual({
155+
url: 'http://localhost:3030/',
156+
headers: expect.any(Object),
157+
});
158+
159+
expect(errorEvent.contexts?.trace).toEqual({
160+
trace_id: pageloadTraceId,
161+
span_id: pageloadSpanId,
162+
});
163+
164+
expect(errorEvent.debug_meta).toEqual({
165+
images: [
166+
{
167+
code_file: expect.stringMatching(/http:\/\/localhost:3030\/assets\/worker3-.+\.js/),
168+
debug_id: expect.stringMatching(/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/),
169+
type: 'sourcemap',
170+
},
171+
],
172+
});
173+
});

packages/browser/src/integrations/webWorker.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { Integration, IntegrationFn } from '@sentry/core';
12
import { debug, defineIntegration, isPlainObject } from '@sentry/core';
23
import { DEBUG_BUILD } from '../debug-build';
34
import { WINDOW } from '../helpers';
@@ -10,21 +11,25 @@ interface WebWorkerMessage {
1011
}
1112

1213
interface WebWorkerIntegrationOptions {
13-
worker: Worker;
14+
worker: Worker | Array<Worker>;
15+
}
16+
17+
interface WebWorkerIntegration extends Integration {
18+
addWorker: (worker: Worker) => void;
1419
}
1520

1621
/**
1722
* Use this integration to set up Sentry with web workers.
1823
*
1924
* IMPORTANT: This integration must be added **before** you start listening to
2025
* any messages from the worker. Otherwise, your message handlers will receive
21-
* messages from Sentry which you need to ignore.
26+
* messages from the Sentry SDK which you need to ignore.
2227
*
2328
* This integration only has an effect, if you call `Sentry.registerWorker(self)`
24-
* from within the worker you're adding to the integration.
29+
* from within the worker(s) you're adding to the integration.
2530
*
2631
* Given that you want to initialize the SDK as early as possible, you most likely
27-
* want to add the integration after initializing the SDK:
32+
* want to add this integration **after** initializing the SDK:
2833
*
2934
* @example:
3035
* ```ts filename={main.js}
@@ -37,14 +42,34 @@ interface WebWorkerIntegrationOptions {
3742
* const worker = new Worker(new URL('./worker.ts', import.meta.url));
3843
*
3944
* // 2. Add the integration
40-
* Sentry.addIntegration(Sentry.webWorkerIntegration({ worker }));
45+
* const webWorkerIntegration = Sentry.webWorkerIntegration({ worker });
46+
* Sentry.addIntegration(webWorkerIntegration);
4147
*
4248
* // 3. Register message listeners on the worker
4349
* worker.addEventListener('message', event => {
4450
* // ...
4551
* });
4652
* ```
4753
*
54+
* If you initialize multiple workers at the same time, you can also pass an array of workers
55+
* to the integration:
56+
*
57+
* ```ts filename={main.js}
58+
* const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: [worker1, worker2] });
59+
* Sentry.addIntegration(webWorkerIntegration);
60+
* ```
61+
*
62+
* If you have any additional workers that you initialize at a later point,
63+
* you can add them to the integration as follows:
64+
*
65+
* ```ts filename={main.js}
66+
* const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: worker1 });
67+
* Sentry.addIntegration(webWorkerIntegration);
68+
*
69+
* // sometime later:
70+
* webWorkerIntegration.addWorker(worker2);
71+
* ```
72+
*
4873
* Of course, you can also directly add the integration in Sentry.init:
4974
* ```ts filename={main.js}
5075
* import * as Sentry from '@sentry/<your-sdk>';
@@ -69,19 +94,24 @@ interface WebWorkerIntegrationOptions {
6994
export const webWorkerIntegration = defineIntegration(({ worker }: WebWorkerIntegrationOptions) => ({
7095
name: INTEGRATION_NAME,
7196
setupOnce: () => {
72-
worker.addEventListener('message', event => {
73-
if (isSentryDebugIdMessage(event.data)) {
74-
event.stopImmediatePropagation(); // other listeners should not receive this message
75-
DEBUG_BUILD && debug.log('Sentry debugId web worker message received', event.data);
76-
WINDOW._sentryDebugIds = {
77-
...event.data._sentryDebugIds,
78-
// debugIds of the main thread have precedence over the worker's in case of a collision.
79-
...WINDOW._sentryDebugIds,
80-
};
81-
}
82-
});
97+
(Array.isArray(worker) ? worker : [worker]).forEach(w => listenForSentryDebugIdMessages(w));
8398
},
84-
}));
99+
addWorker: (worker: Worker) => listenForSentryDebugIdMessages(worker),
100+
})) as IntegrationFn<WebWorkerIntegration>;
101+
102+
function listenForSentryDebugIdMessages(worker: Worker): void {
103+
worker.addEventListener('message', event => {
104+
if (isSentryDebugIdMessage(event.data)) {
105+
event.stopImmediatePropagation(); // other listeners should not receive this message
106+
DEBUG_BUILD && debug.log('Sentry debugId web worker message received', event.data);
107+
WINDOW._sentryDebugIds = {
108+
...event.data._sentryDebugIds,
109+
// debugIds of the main thread have precedence over the worker's in case of a collision.
110+
...WINDOW._sentryDebugIds,
111+
};
112+
}
113+
});
114+
}
85115

86116
interface RegisterWebWorkerOptions {
87117
self: Worker & { _sentryDebugIds?: Record<string, string> };

0 commit comments

Comments
 (0)