@@ -72,12 +72,16 @@ const dispatchUnhandledRejection = (
7272 window . dispatchEvent ( event ) ;
7373} ;
7474
75- // The instrumentation does not expose its diag logger for testing, so reach
76- // the internal `_diag` through a single typed accessor rather than casting in
77- // each test.
75+ // The instrumentation does not expose its `_diag` or ` logger` fields for
76+ // testing, so reach the internals through a single typed accessor rather than
77+ // casting in each test.
7878const internals = ( inst : ErrorsInstrumentation | undefined ) =>
7979 inst as unknown as {
80- _diag : { debug : ( ...args : unknown [ ] ) => void } ;
80+ _diag : {
81+ debug : ( ...args : unknown [ ] ) => void ;
82+ error : ( ...args : unknown [ ] ) => void ;
83+ } ;
84+ logger : { emit : ( record : unknown ) => void } ;
8185 } ;
8286
8387describe ( 'ErrorsInstrumentation' , ( ) => {
@@ -403,4 +407,63 @@ describe('ErrorsInstrumentation', () => {
403407 ) ;
404408 } ) ;
405409 } ) ;
410+
411+ describe ( 'failure containment' , ( ) => {
412+ beforeEach ( ( ) => {
413+ instrumentation = new ErrorsInstrumentation ( { enabled : false } ) ;
414+ instrumentation . enable ( ) ;
415+ } ) ;
416+
417+ it ( 'should contain a throwing LogRecordProcessor and surface it via diag' , ( ) => {
418+ const accessor = internals ( instrumentation ) ;
419+ const diagSpy = vi . spyOn ( accessor . _diag , 'error' ) ;
420+ const emitSpy = vi
421+ . spyOn ( accessor . logger , 'emit' )
422+ . mockImplementation ( ( ) => {
423+ throw new Error ( 'processor exploded' ) ;
424+ } ) ;
425+
426+ try {
427+ // A throw escaping the listener would re-trigger 'error' and loop.
428+ expect ( ( ) =>
429+ dispatchErrorEvent ( new ValidationError ( 'boom' ) ) ,
430+ ) . not . toThrow ( ) ;
431+
432+ // Exactly once: the contained throw did not re-enter the listener.
433+ expect ( emitSpy ) . toHaveBeenCalledTimes ( 1 ) ;
434+ expect ( diagSpy ) . toHaveBeenCalledWith (
435+ 'failed to record exception' ,
436+ expect . any ( Error ) ,
437+ ) ;
438+ } finally {
439+ emitSpy . mockRestore ( ) ;
440+ diagSpy . mockRestore ( ) ;
441+ }
442+ } ) ;
443+
444+ it ( 'should contain a rejection reason that throws while its properties are read' , ( ) => {
445+ const diagSpy = vi . spyOn ( internals ( instrumentation ) . _diag , 'error' ) ;
446+
447+ // A promise can reject with any value. This one throws while its `stack`
448+ // is read, exercising the extraction path before emit is ever reached.
449+ const hostileReason = { } as Error ;
450+ Object . defineProperty ( hostileReason , 'stack' , {
451+ get ( ) {
452+ throw new Error ( 'stack getter exploded' ) ;
453+ } ,
454+ } ) ;
455+
456+ try {
457+ expect ( ( ) => dispatchUnhandledRejection ( hostileReason ) ) . not . toThrow ( ) ;
458+
459+ expect ( getErrorLogs ( ) ) . toHaveLength ( 0 ) ;
460+ expect ( diagSpy ) . toHaveBeenCalledWith (
461+ 'failed to record exception' ,
462+ expect . any ( Error ) ,
463+ ) ;
464+ } finally {
465+ diagSpy . mockRestore ( ) ;
466+ }
467+ } ) ;
468+ } ) ;
406469} ) ;
0 commit comments