11import type { Integration , IntegrationFn } from '@sentry/core' ;
2- import { debug , defineIntegration , isPlainObject } from '@sentry/core' ;
2+ import { captureEvent , debug , defineIntegration , getClient , isPlainObject , isPrimitive } from '@sentry/core' ;
33import { DEBUG_BUILD } from '../debug-build' ;
4+ import { eventFromUnknownInput } from '../eventbuilder' ;
45import { WINDOW } from '../helpers' ;
6+ import { _eventFromRejectionWithPrimitive , _getUnhandledRejectionError } from './globalhandlers' ;
57
68export const INTEGRATION_NAME = 'WebWorker' ;
79
810interface WebWorkerMessage {
911 _sentryMessage : boolean ;
1012 _sentryDebugIds ?: Record < string , string > ;
13+ _sentryWorkerError ?: SerializedWorkerError ;
14+ }
15+
16+ interface SerializedWorkerError {
17+ reason : unknown ;
18+ filename ?: string ;
1119}
1220
1321interface WebWorkerIntegrationOptions {
@@ -94,25 +102,75 @@ interface WebWorkerIntegration extends Integration {
94102export const webWorkerIntegration = defineIntegration ( ( { worker } : WebWorkerIntegrationOptions ) => ( {
95103 name : INTEGRATION_NAME ,
96104 setupOnce : ( ) => {
97- ( Array . isArray ( worker ) ? worker : [ worker ] ) . forEach ( w => listenForSentryDebugIdMessages ( w ) ) ;
105+ ( Array . isArray ( worker ) ? worker : [ worker ] ) . forEach ( w => listenForSentryMessages ( w ) ) ;
98106 } ,
99- addWorker : ( worker : Worker ) => listenForSentryDebugIdMessages ( worker ) ,
107+ addWorker : ( worker : Worker ) => listenForSentryMessages ( worker ) ,
100108} ) ) as IntegrationFn < WebWorkerIntegration > ;
101109
102- function listenForSentryDebugIdMessages ( worker : Worker ) : void {
110+ function listenForSentryMessages ( worker : Worker ) : void {
103111 worker . addEventListener ( 'message' , event => {
104- if ( isSentryDebugIdMessage ( event . data ) ) {
112+ if ( isSentryMessage ( event . data ) ) {
105113 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- } ;
114+
115+ // Handle debug IDs
116+ if ( event . data . _sentryDebugIds ) {
117+ DEBUG_BUILD && debug . log ( 'Sentry debugId web worker message received' , event . data ) ;
118+ WINDOW . _sentryDebugIds = {
119+ ...event . data . _sentryDebugIds ,
120+ // debugIds of the main thread have precedence over the worker's in case of a collision.
121+ ...WINDOW . _sentryDebugIds ,
122+ } ;
123+ }
124+
125+ // Handle unhandled rejections forwarded from worker
126+ if ( event . data . _sentryWorkerError ) {
127+ DEBUG_BUILD && debug . log ( 'Sentry worker rejection message received' , event . data . _sentryWorkerError ) ;
128+ handleForwardedWorkerRejection ( event . data . _sentryWorkerError ) ;
129+ }
112130 }
113131 } ) ;
114132}
115133
134+ function handleForwardedWorkerRejection ( workerError : SerializedWorkerError ) : void {
135+ const client = getClient ( ) ;
136+ if ( ! client ) {
137+ return ;
138+ }
139+
140+ const stackParser = client . getOptions ( ) . stackParser ;
141+ const attachStacktrace = client . getOptions ( ) . attachStacktrace ;
142+
143+ const error = workerError . reason ;
144+
145+ // Follow same pattern as globalHandlers for unhandledrejection
146+ // Handle both primitives and errors the same way
147+ const event = isPrimitive ( error )
148+ ? _eventFromRejectionWithPrimitive ( error )
149+ : eventFromUnknownInput ( stackParser , error , undefined , attachStacktrace , true ) ;
150+
151+ event . level = 'error' ;
152+
153+ // Add worker-specific context
154+ if ( workerError . filename ) {
155+ event . contexts = {
156+ ...event . contexts ,
157+ worker : {
158+ filename : workerError . filename ,
159+ } ,
160+ } ;
161+ }
162+
163+ captureEvent ( event , {
164+ originalException : error ,
165+ mechanism : {
166+ handled : false ,
167+ type : 'auto.browser.web_worker.onunhandledrejection' ,
168+ } ,
169+ } ) ;
170+
171+ DEBUG_BUILD && debug . log ( 'Captured worker unhandled rejection' , error ) ;
172+ }
173+
116174/**
117175 * Minimal interface for DedicatedWorkerGlobalScope, only requiring the postMessage method.
118176 * (which is the only thing we need from the worker's global object)
@@ -124,6 +182,8 @@ function listenForSentryDebugIdMessages(worker: Worker): void {
124182 */
125183interface MinimalDedicatedWorkerGlobalScope {
126184 postMessage : ( message : unknown ) => void ;
185+ addEventListener : ( type : string , listener : ( event : unknown ) => void ) => void ;
186+ location ?: { href ?: string } ;
127187}
128188
129189interface RegisterWebWorkerOptions {
@@ -133,6 +193,14 @@ interface RegisterWebWorkerOptions {
133193/**
134194 * Use this function to register the worker with the Sentry SDK.
135195 *
196+ * This function will:
197+ * - Send debug IDs to the parent thread
198+ * - Set up a handler for unhandled rejections in the worker
199+ * - Forward unhandled rejections to the parent thread for capture
200+ *
201+ * Note: Synchronous errors in workers are already captured by globalHandlers.
202+ * This only handles unhandled promise rejections which don't bubble to the parent.
203+ *
136204 * @example
137205 * ```ts filename={worker.js}
138206 * import * as Sentry from '@sentry/<your-sdk>';
@@ -147,17 +215,59 @@ interface RegisterWebWorkerOptions {
147215 * - `self`: The worker instance you're calling this function from (self).
148216 */
149217export function registerWebWorker ( { self } : RegisterWebWorkerOptions ) : void {
218+ // Send debug IDs to parent thread
150219 self . postMessage ( {
151220 _sentryMessage : true ,
152221 _sentryDebugIds : self . _sentryDebugIds ?? undefined ,
153222 } ) ;
223+
224+ // Set up unhandledrejection handler inside the worker
225+ // Following the same pattern as globalHandlers
226+ // unhandled rejections don't bubble to the parent thread, so we need to handle them here
227+ self . addEventListener ( 'unhandledrejection' , ( event : unknown ) => {
228+ const reason = _getUnhandledRejectionError ( event ) ;
229+
230+ // Forward the raw reason to parent thread
231+ // The parent will handle primitives vs errors the same way globalHandlers does
232+ const serializedError : SerializedWorkerError = {
233+ reason : reason ,
234+ filename : self . location ?. href ,
235+ } ;
236+
237+ // Forward to parent thread
238+ self . postMessage ( {
239+ _sentryMessage : true ,
240+ _sentryWorkerError : serializedError ,
241+ } ) ;
242+
243+ DEBUG_BUILD && debug . log ( '[Sentry Worker] Forwarding unhandled rejection to parent' , serializedError ) ;
244+ } ) ;
245+
246+ DEBUG_BUILD && debug . log ( '[Sentry Worker] Registered worker with unhandled rejection handling' ) ;
154247}
155248
156- function isSentryDebugIdMessage ( eventData : unknown ) : eventData is WebWorkerMessage {
157- return (
158- isPlainObject ( eventData ) &&
159- eventData . _sentryMessage === true &&
160- '_sentryDebugIds' in eventData &&
161- ( isPlainObject ( eventData . _sentryDebugIds ) || eventData . _sentryDebugIds === undefined )
162- ) ;
249+ function isSentryMessage ( eventData : unknown ) : eventData is WebWorkerMessage {
250+ if ( ! isPlainObject ( eventData ) || eventData . _sentryMessage !== true ) {
251+ return false ;
252+ }
253+
254+ // Must have at least one of: debug IDs or worker error
255+ const hasDebugIds = '_sentryDebugIds' in eventData ;
256+ const hasWorkerError = '_sentryWorkerError' in eventData ;
257+
258+ if ( ! hasDebugIds && ! hasWorkerError ) {
259+ return false ;
260+ }
261+
262+ // Validate debug IDs if present
263+ if ( hasDebugIds && ! ( isPlainObject ( eventData . _sentryDebugIds ) || eventData . _sentryDebugIds === undefined ) ) {
264+ return false ;
265+ }
266+
267+ // Validate worker error if present
268+ if ( hasWorkerError && ! isPlainObject ( eventData . _sentryWorkerError ) ) {
269+ return false ;
270+ }
271+
272+ return true ;
163273}
0 commit comments