@@ -23,8 +23,8 @@ const createMockExecutionContext = (): MockExecutionContext => {
2323 } as unknown as MockExecutionContext
2424}
2525
26- const createClient = ( events : EventDeliveryPayload [ ] , sessions : SessionDeliveryPayload [ ] , config = { } ) => {
27- const client = new Client ( { apiKey : 'AN_API_KEY' , plugins : [ BugsnagPluginCloudflareWorkers ] , ...config } )
26+ const createClient = ( events : EventDeliveryPayload [ ] , sessions : SessionDeliveryPayload [ ] , config = { } , additionalPlugins : any [ ] = [ ] ) => {
27+ const client = new Client ( { apiKey : 'AN_API_KEY' , plugins : [ BugsnagPluginCloudflareWorkers , ... additionalPlugins ] , ...config } )
2828
2929 // @ts -ignore the following property is not defined on the public Event interface
3030 client . Event . __type = 'nodejs'
@@ -40,6 +40,12 @@ const createClient = (events: EventDeliveryPayload[], sessions: SessionDeliveryP
4040 }
4141 }
4242
43+ // Mock _clientContext.run to execute the callback immediately
44+ // This simulates AsyncLocalStorage behavior for testing
45+ client . _clientContext = {
46+ run : jest . fn ( ( requestClient , callback ) => callback ( ) )
47+ }
48+
4349 return client
4450}
4551
@@ -97,6 +103,13 @@ describe('plugin: cloudflare workers', () => {
97103 // Wait for promises registered with ctx.waitUntil to complete
98104 await ctx . _waitForAllPromises ( )
99105
106+ // Verify _clientContext.run was called with a cloned client
107+ expect ( client . _clientContext . run ) . toHaveBeenCalledTimes ( 1 )
108+ expect ( client . _clientContext . run ) . toHaveBeenCalledWith (
109+ expect . any ( Client ) ,
110+ expect . any ( Function )
111+ )
112+
100113 expect ( events ) . toHaveLength ( 1 )
101114
102115 const event = events [ 0 ] . events [ 0 ]
@@ -139,6 +152,11 @@ describe('plugin: cloudflare workers', () => {
139152 }
140153 }
141154
155+ // Mock _clientContext.run to execute the callback immediately
156+ client . _clientContext = {
157+ run : jest . fn ( ( requestClient , callback ) => callback ( ) )
158+ }
159+
142160 const timeoutError = new Error ( 'flush timed out after 20ms' )
143161
144162 const plugin = client . getPlugin ( 'cloudflareWorkers' )
@@ -200,6 +218,13 @@ describe('plugin: cloudflare workers', () => {
200218 const response = await exportedHandler . fetch ?.( request , env , ctx )
201219 expect ( await response ?. text ( ) ) . toBe ( 'Hello World! test value' )
202220
221+ // Verify _clientContext.run was called
222+ expect ( client . _clientContext . run ) . toHaveBeenCalledTimes ( 1 )
223+ expect ( client . _clientContext . run ) . toHaveBeenCalledWith (
224+ expect . any ( Client ) ,
225+ expect . any ( Function )
226+ )
227+
203228 expect ( events ) . toHaveLength ( 0 )
204229 expect ( sessions ) . toHaveLength ( 1 )
205230 } )
@@ -235,6 +260,9 @@ describe('plugin: cloudflare workers', () => {
235260 // Wait for promises registered with ctx.waitUntil to complete
236261 await ctx . _waitForAllPromises ( )
237262
263+ // Verify _clientContext.run was called
264+ expect ( client . _clientContext . run ) . toHaveBeenCalledTimes ( 1 )
265+
238266 expect ( events ) . toHaveLength ( 1 )
239267
240268 const event = events [ 0 ] . events [ 0 ]
@@ -313,4 +341,163 @@ describe('plugin: cloudflare workers', () => {
313341 expect ( events ) . toHaveLength ( 0 )
314342 expect ( sessions ) . toHaveLength ( 1 )
315343 } )
344+
345+ it ( 'clones the client for each request to avoid callback accumulation' , async ( ) => {
346+ const events : EventDeliveryPayload [ ] = [ ]
347+ const sessions : SessionDeliveryPayload [ ] = [ ]
348+
349+ const client = createClient ( events , sessions )
350+
351+ const plugin = client . getPlugin ( 'cloudflareWorkers' )
352+
353+ if ( ! plugin ) {
354+ throw new Error ( 'Plugin was not loaded!' )
355+ }
356+
357+ const bugsnagHandler = plugin . createHandler ( )
358+
359+ const exportedHandler : ExportedHandler < Env > = {
360+ fetch : bugsnagHandler ( async ( request , env , ctx ) => {
361+ throw new Error ( 'Test error' )
362+ } )
363+ }
364+
365+ const request1 = new Request ( 'https://example.com/request1?param1=value1' , {
366+ method : 'GET' ,
367+ headers : {
368+ 'X-Custom-Header' : 'request1'
369+ }
370+ } ) as unknown as CloudflareRequest < unknown , IncomingRequestCfProperties < unknown > >
371+ const request2 = new Request ( 'https://example.com/request2?param2=value2' , {
372+ method : 'POST' ,
373+ headers : {
374+ 'X-Custom-Header' : 'request2'
375+ }
376+ } ) as unknown as CloudflareRequest < unknown , IncomingRequestCfProperties < unknown > >
377+ const env = { }
378+ const ctx1 = createMockExecutionContext ( )
379+ const ctx2 = createMockExecutionContext ( )
380+
381+ // Make two requests (both will throw errors)
382+ await expect ( exportedHandler . fetch ?.( request1 , env , ctx1 ) ) . rejects . toThrow ( 'Test error' )
383+ await expect ( exportedHandler . fetch ?.( request2 , env , ctx2 ) ) . rejects . toThrow ( 'Test error' )
384+
385+ // Wait for promises registered with ctx.waitUntil to complete
386+ await ctx1 . _waitForAllPromises ( )
387+ await ctx2 . _waitForAllPromises ( )
388+
389+ // Verify _clientContext.run was called twice with different cloned clients
390+ expect ( client . _clientContext . run ) . toHaveBeenCalledTimes ( 2 )
391+
392+ const firstCallClient = ( client . _clientContext . run as jest . Mock ) . mock . calls [ 0 ] [ 0 ]
393+ const secondCallClient = ( client . _clientContext . run as jest . Mock ) . mock . calls [ 1 ] [ 0 ]
394+
395+ // Both should be Client instances but different instances
396+ expect ( firstCallClient ) . toBeInstanceOf ( Client )
397+ expect ( secondCallClient ) . toBeInstanceOf ( Client )
398+ expect ( firstCallClient ) . not . toBe ( secondCallClient )
399+ expect ( firstCallClient ) . not . toBe ( client )
400+ expect ( secondCallClient ) . not . toBe ( client )
401+
402+ // Both requests should have started sessions
403+ expect ( sessions ) . toHaveLength ( 2 )
404+
405+ // Verify two events were sent
406+ expect ( events ) . toHaveLength ( 2 )
407+
408+ // Verify first event has correct request metadata
409+ const event1 = events [ 0 ] . events [ 0 ]
410+ expect ( event1 . request ) . toMatchObject ( {
411+ url : 'https://example.com/request1?param1=value1' ,
412+ httpMethod : 'GET' ,
413+ headers : expect . objectContaining ( {
414+ 'x-custom-header' : 'request1'
415+ } )
416+ } )
417+ // @ts -ignore
418+ expect ( event1 . _metadata ?. request ) . toMatchObject ( {
419+ url : 'https://example.com/request1?param1=value1' ,
420+ path : '/request1' ,
421+ httpMethod : 'GET' ,
422+ query : {
423+ param1 : 'value1'
424+ } ,
425+ headers : expect . objectContaining ( {
426+ 'x-custom-header' : 'request1'
427+ } )
428+ } )
429+
430+ // Verify second event has correct request metadata
431+ const event2 = events [ 1 ] . events [ 0 ]
432+ expect ( event2 . request ) . toMatchObject ( {
433+ url : 'https://example.com/request2?param2=value2' ,
434+ httpMethod : 'POST' ,
435+ headers : expect . objectContaining ( {
436+ 'x-custom-header' : 'request2'
437+ } )
438+ } )
439+ // @ts -ignore
440+ expect ( event2 . _metadata ?. request ) . toMatchObject ( {
441+ url : 'https://example.com/request2?param2=value2' ,
442+ path : '/request2' ,
443+ httpMethod : 'POST' ,
444+ query : {
445+ param2 : 'value2'
446+ } ,
447+ headers : expect . objectContaining ( {
448+ 'x-custom-header' : 'request2'
449+ } )
450+ } )
451+ } )
452+
453+ it ( 'resets app duration plugin between requests' , async ( ) => {
454+ const events : EventDeliveryPayload [ ] = [ ]
455+ const sessions : SessionDeliveryPayload [ ] = [ ]
456+
457+ // Mock the app duration plugin
458+ const mockReset = jest . fn ( )
459+ const mockAppDurationPlugin = {
460+ name : 'appDuration' ,
461+ load : ( ) => ( {
462+ reset : mockReset
463+ } )
464+ }
465+
466+ const client = createClient ( events , sessions , { } , [ mockAppDurationPlugin ] )
467+
468+ const plugin = client . getPlugin ( 'cloudflareWorkers' )
469+
470+ if ( ! plugin ) {
471+ throw new Error ( 'Plugin was not loaded!' )
472+ }
473+
474+ const bugsnagHandler = plugin . createHandler ( )
475+
476+ const exportedHandler : ExportedHandler < Env > = {
477+ fetch : bugsnagHandler ( async ( request , env , ctx ) => {
478+ return new Response ( 'OK' ) as unknown as CloudflareResponse
479+ } )
480+ }
481+
482+ const request1 = new Request ( 'https://example.com/request1' ) as unknown as CloudflareRequest < unknown , IncomingRequestCfProperties < unknown > >
483+ const request2 = new Request ( 'https://example.com/request2' ) as unknown as CloudflareRequest < unknown , IncomingRequestCfProperties < unknown > >
484+ const request3 = new Request ( 'https://example.com/request3' ) as unknown as CloudflareRequest < unknown , IncomingRequestCfProperties < unknown > >
485+ const env = { }
486+ const ctx1 = createMockExecutionContext ( )
487+ const ctx2 = createMockExecutionContext ( )
488+ const ctx3 = createMockExecutionContext ( )
489+
490+ // Make three requests
491+ await exportedHandler . fetch ?.( request1 , env , ctx1 )
492+ await exportedHandler . fetch ?.( request2 , env , ctx2 )
493+ await exportedHandler . fetch ?.( request3 , env , ctx3 )
494+
495+ // Wait for all promises to complete
496+ await ctx1 . _waitForAllPromises ( )
497+ await ctx2 . _waitForAllPromises ( )
498+ await ctx3 . _waitForAllPromises ( )
499+
500+ // Verify reset was called for each request
501+ expect ( mockReset ) . toHaveBeenCalledTimes ( 3 )
502+ } )
316503} )
0 commit comments