@sentry/browser
Version:
Official Sentry SDK for browsers
248 lines (220 loc) • 8.13 kB
JavaScript
import { defineIntegration, debug, getClient, isPrimitive, captureEvent, isPlainObject } from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build.js';
import { eventFromUnknownInput } from '../eventbuilder.js';
import { WINDOW } from '../helpers.js';
import { _eventFromRejectionWithPrimitive, _getUnhandledRejectionError } from './globalhandlers.js';
const INTEGRATION_NAME = 'WebWorker';
/**
* Use this integration to set up Sentry with web workers.
*
* IMPORTANT: This integration must be added **before** you start listening to
* any messages from the worker. Otherwise, your message handlers will receive
* messages from the Sentry SDK which you need to ignore.
*
* This integration only has an effect, if you call `Sentry.registerWebWorker(self)`
* from within the worker(s) you're adding to the integration.
*
* Given that you want to initialize the SDK as early as possible, you most likely
* want to add this integration **after** initializing the SDK:
*
* @example:
* ```ts filename={main.js}
* import * as Sentry from '@sentry/<your-sdk>';
*
* // some time earlier:
* Sentry.init(...)
*
* // 1. Initialize the worker
* const worker = new Worker(new URL('./worker.ts', import.meta.url));
*
* // 2. Add the integration
* const webWorkerIntegration = Sentry.webWorkerIntegration({ worker });
* Sentry.addIntegration(webWorkerIntegration);
*
* // 3. Register message listeners on the worker
* worker.addEventListener('message', event => {
* // ...
* });
* ```
*
* If you initialize multiple workers at the same time, you can also pass an array of workers
* to the integration:
*
* ```ts filename={main.js}
* const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: [worker1, worker2] });
* Sentry.addIntegration(webWorkerIntegration);
* ```
*
* If you have any additional workers that you initialize at a later point,
* you can add them to the integration as follows:
*
* ```ts filename={main.js}
* const webWorkerIntegration = Sentry.webWorkerIntegration({ worker: worker1 });
* Sentry.addIntegration(webWorkerIntegration);
*
* // sometime later:
* webWorkerIntegration.addWorker(worker2);
* ```
*
* Of course, you can also directly add the integration in Sentry.init:
* ```ts filename={main.js}
* import * as Sentry from '@sentry/<your-sdk>';
*
* // 1. Initialize the worker
* const worker = new Worker(new URL('./worker.ts', import.meta.url));
*
* // 2. Initialize the SDK
* Sentry.init({
* integrations: [Sentry.webWorkerIntegration({ worker })]
* });
*
* // 3. Register message listeners on the worker
* worker.addEventListener('message', event => {
* // ...
* });
* ```
*
* @param options {WebWorkerIntegrationOptions} Integration options:
* - `worker`: The worker instance.
*/
const webWorkerIntegration = defineIntegration(({ worker }) => ({
name: INTEGRATION_NAME,
setupOnce: () => {
(Array.isArray(worker) ? worker : [worker]).forEach(w => listenForSentryMessages(w));
},
addWorker: (worker) => listenForSentryMessages(worker),
})) ;
function listenForSentryMessages(worker) {
worker.addEventListener('message', event => {
if (isSentryMessage(event.data)) {
event.stopImmediatePropagation(); // other listeners should not receive this message
// Handle debug IDs
if (event.data._sentryDebugIds) {
DEBUG_BUILD && debug.log('Sentry debugId web worker message received', event.data);
WINDOW._sentryDebugIds = {
...event.data._sentryDebugIds,
// debugIds of the main thread have precedence over the worker's in case of a collision.
...WINDOW._sentryDebugIds,
};
}
// Handle unhandled rejections forwarded from worker
if (event.data._sentryWorkerError) {
DEBUG_BUILD && debug.log('Sentry worker rejection message received', event.data._sentryWorkerError);
handleForwardedWorkerRejection(event.data._sentryWorkerError);
}
}
});
}
function handleForwardedWorkerRejection(workerError) {
const client = getClient();
if (!client) {
return;
}
const stackParser = client.getOptions().stackParser;
const attachStacktrace = client.getOptions().attachStacktrace;
const error = workerError.reason;
// Follow same pattern as globalHandlers for unhandledrejection
// Handle both primitives and errors the same way
const event = isPrimitive(error)
? _eventFromRejectionWithPrimitive(error)
: eventFromUnknownInput(stackParser, error, undefined, attachStacktrace, true);
event.level = 'error';
// Add worker-specific context
if (workerError.filename) {
event.contexts = {
...event.contexts,
worker: {
filename: workerError.filename,
},
};
}
captureEvent(event, {
originalException: error,
mechanism: {
handled: false,
type: 'auto.browser.web_worker.onunhandledrejection',
},
});
DEBUG_BUILD && debug.log('Captured worker unhandled rejection', error);
}
/**
* Minimal interface for DedicatedWorkerGlobalScope, only requiring the postMessage method.
* (which is the only thing we need from the worker's global object)
*
* @see https://developer.mozilla.org/en-US/docs/Web/API/DedicatedWorkerGlobalScope
*
* We can't use the actual type because it breaks everyone who doesn't have {"lib": ["WebWorker"]}
* but uses {"skipLibCheck": true} in their tsconfig.json.
*/
/**
* Use this function to register the worker with the Sentry SDK.
*
* This function will:
* - Send debug IDs to the parent thread
* - Set up a handler for unhandled rejections in the worker
* - Forward unhandled rejections to the parent thread for capture
*
* Note: Synchronous errors in workers are already captured by globalHandlers.
* This only handles unhandled promise rejections which don't bubble to the parent.
*
* @example
* ```ts filename={worker.js}
* import * as Sentry from '@sentry/<your-sdk>';
*
* // Do this as early as possible in your worker.
* Sentry.registerWebWorker({ self });
*
* // continue setting up your worker
* self.postMessage(...)
* ```
* @param options {RegisterWebWorkerOptions} Integration options:
* - `self`: The worker instance you're calling this function from (self).
*/
function registerWebWorker({ self }) {
// Send debug IDs to parent thread
self.postMessage({
_sentryMessage: true,
_sentryDebugIds: self._sentryDebugIds ?? undefined,
});
// Set up unhandledrejection handler inside the worker
// Following the same pattern as globalHandlers
// unhandled rejections don't bubble to the parent thread, so we need to handle them here
self.addEventListener('unhandledrejection', (event) => {
const reason = _getUnhandledRejectionError(event);
// Forward the raw reason to parent thread
// The parent will handle primitives vs errors the same way globalHandlers does
const serializedError = {
reason: reason,
filename: self.location?.href,
};
// Forward to parent thread
self.postMessage({
_sentryMessage: true,
_sentryWorkerError: serializedError,
});
DEBUG_BUILD && debug.log('[Sentry Worker] Forwarding unhandled rejection to parent', serializedError);
});
DEBUG_BUILD && debug.log('[Sentry Worker] Registered worker with unhandled rejection handling');
}
function isSentryMessage(eventData) {
if (!isPlainObject(eventData) || eventData._sentryMessage !== true) {
return false;
}
// Must have at least one of: debug IDs or worker error
const hasDebugIds = '_sentryDebugIds' in eventData;
const hasWorkerError = '_sentryWorkerError' in eventData;
if (!hasDebugIds && !hasWorkerError) {
return false;
}
// Validate debug IDs if present
if (hasDebugIds && !(isPlainObject(eventData._sentryDebugIds) || eventData._sentryDebugIds === undefined)) {
return false;
}
// Validate worker error if present
if (hasWorkerError && !isPlainObject(eventData._sentryWorkerError)) {
return false;
}
return true;
}
export { INTEGRATION_NAME, registerWebWorker, webWorkerIntegration };
//# sourceMappingURL=webWorker.js.map