@@ -407,6 +407,94 @@ describe('bindTracingChannelToSpan', () => {
407407 expect ( spanToJSON ( span ) . status ) . toBe ( 'db-down' ) ;
408408 expect ( spanToJSON ( span ) . timestamp ) . toBeDefined ( ) ;
409409 } ) ;
410+
411+ it ( 'captures the exception with the default mechanism when `captureError` is true' , async ( ) => {
412+ installTestAsyncContextStrategy ( ) ;
413+ initTestClient ( ) ;
414+ const captureExceptionSpy = vi . spyOn ( SentryCore , 'captureException' ) . mockReturnValue ( 'event-id' ) ;
415+
416+ const span = startInactiveSpan ( { name : 'channel-span' } ) ;
417+ const error = new Error ( 'boom' ) ;
418+ const { channel } = bindTracingChannelToSpan (
419+ tracingChannel < { operation : string } > ( 'test:captureError:true' ) ,
420+ ( ) => span ,
421+ { captureError : true } ,
422+ ) ;
423+
424+ await expect (
425+ channel . tracePromise (
426+ async ( ) => {
427+ throw error ;
428+ } ,
429+ { operation : 'read' } ,
430+ ) ,
431+ ) . rejects . toThrow ( error ) ;
432+
433+ expect ( captureExceptionSpy ) . toHaveBeenCalledTimes ( 1 ) ;
434+ expect ( captureExceptionSpy ) . toHaveBeenCalledWith ( error , {
435+ mechanism : { type : 'auto.diagnostic_channels.bind_span' , handled : false } ,
436+ } ) ;
437+ expect ( spanToJSON ( span ) . status ) . toBe ( 'boom' ) ;
438+ } ) ;
439+
440+ it ( 'captures the exception with the hint returned by a `captureError` function, passing it the thrown error' , async ( ) => {
441+ installTestAsyncContextStrategy ( ) ;
442+ initTestClient ( ) ;
443+ const captureExceptionSpy = vi . spyOn ( SentryCore , 'captureException' ) . mockReturnValue ( 'event-id' ) ;
444+
445+ const span = startInactiveSpan ( { name : 'channel-span' } ) ;
446+ const error = new Error ( 'boom' ) ;
447+ const captureError = vi . fn ( ( ) => ( { mechanism : { type : 'auto.http.custom' , handled : false } } ) ) ;
448+ const { channel } = bindTracingChannelToSpan (
449+ tracingChannel < { operation : string } > ( 'test:captureError:fn' ) ,
450+ ( ) => span ,
451+ { captureError } ,
452+ ) ;
453+
454+ await expect (
455+ channel . tracePromise (
456+ async ( ) => {
457+ throw error ;
458+ } ,
459+ { operation : 'read' } ,
460+ ) ,
461+ ) . rejects . toThrow ( error ) ;
462+
463+ expect ( captureError ) . toHaveBeenCalledTimes ( 1 ) ;
464+ expect ( captureError ) . toHaveBeenCalledWith ( error ) ;
465+ expect ( captureExceptionSpy ) . toHaveBeenCalledTimes ( 1 ) ;
466+ expect ( captureExceptionSpy ) . toHaveBeenCalledWith ( error , {
467+ mechanism : { type : 'auto.http.custom' , handled : false } ,
468+ } ) ;
469+ expect ( spanToJSON ( span ) . status ) . toBe ( 'boom' ) ;
470+ } ) ;
471+
472+ it ( 'uses the default mechanism when `captureError` is a function on the synchronous error path' , ( ) => {
473+ installTestAsyncContextStrategy ( ) ;
474+ initTestClient ( ) ;
475+ const captureExceptionSpy = vi . spyOn ( SentryCore , 'captureException' ) . mockReturnValue ( 'event-id' ) ;
476+
477+ const span = startInactiveSpan ( { name : 'channel-span' } ) ;
478+ const error = new Error ( 'sync-boom' ) ;
479+ const captureError = vi . fn ( ( e : unknown ) => ( { extra : { caught : e instanceof Error } } ) ) ;
480+ const { channel } = bindTracingChannelToSpan (
481+ tracingChannel < { operation : string } > ( 'test:captureError:fn-sync' ) ,
482+ ( ) => span ,
483+ { captureError } ,
484+ ) ;
485+
486+ expect ( ( ) =>
487+ channel . traceSync (
488+ ( ) => {
489+ throw error ;
490+ } ,
491+ { operation : 'read' } ,
492+ ) ,
493+ ) . toThrow ( error ) ;
494+
495+ expect ( captureError ) . toHaveBeenCalledWith ( error ) ;
496+ expect ( captureExceptionSpy ) . toHaveBeenCalledWith ( error , { extra : { caught : true } } ) ;
497+ } ) ;
410498 } ) ;
411499
412500 describe ( 'beforeSpanEnd' , ( ) => {
@@ -566,4 +654,64 @@ describe('bindTracingChannelToSpan', () => {
566654 // Idempotent.
567655 expect ( ( ) => unbind ( ) ) . not . toThrow ( ) ;
568656 } ) ;
657+
658+ describe ( 'getSpan returns undefined' , ( ) => {
659+ it ( 'skips binding and lifecycle, leaving the enclosing span as the active parent' , ( ) => {
660+ installTestAsyncContextStrategy ( ) ;
661+ initTestClient ( ) ;
662+
663+ const getSpan = vi . fn ( ( ) => undefined ) ;
664+ const { channel } = bindTracingChannelToSpan ( tracingChannel < { operation : string } > ( 'test:skip:active' ) , getSpan ) ;
665+
666+ let dataSpan : Span | undefined ;
667+ channel . subscribe ( {
668+ end : data => {
669+ dataSpan = data . _sentrySpan ;
670+ } ,
671+ } ) ;
672+
673+ let enclosingSpanId : string | undefined ;
674+ let childParentSpanId : string | undefined ;
675+ startSpan ( { forceTransaction : true , name : 'enclosing-span' } , enclosing => {
676+ enclosingSpanId = enclosing . spanContext ( ) . spanId ;
677+ channel . traceSync (
678+ ( ) => {
679+ startSpan ( { name : 'child-span' } , child => {
680+ childParentSpanId = spanToJSON ( child ) . parent_span_id ;
681+ } ) ;
682+ } ,
683+ { operation : 'read' } ,
684+ ) ;
685+ } ) ;
686+
687+ expect ( getSpan ) . toHaveBeenCalledTimes ( 1 ) ;
688+ // No span was stamped onto the payload, so the lifecycle handlers have nothing to end.
689+ expect ( dataSpan ) . toBeUndefined ( ) ;
690+ // The context is left untouched, so children still parent to the enclosing span.
691+ expect ( childParentSpanId ) . toBe ( enclosingSpanId ) ;
692+ } ) ;
693+
694+ it ( 'does not capture the exception on the error path when no span was bound' , async ( ) => {
695+ installTestAsyncContextStrategy ( ) ;
696+ initTestClient ( ) ;
697+ const captureExceptionSpy = vi . spyOn ( SentryCore , 'captureException' ) . mockReturnValue ( 'event-id' ) ;
698+
699+ const { channel } = bindTracingChannelToSpan (
700+ tracingChannel < { operation : string } > ( 'test:skip:error' ) ,
701+ ( ) => undefined ,
702+ ) ;
703+
704+ const error = new Error ( 'boom' ) ;
705+ await expect (
706+ channel . tracePromise (
707+ async ( ) => {
708+ throw error ;
709+ } ,
710+ { operation : 'read' } ,
711+ ) ,
712+ ) . rejects . toThrow ( error ) ;
713+
714+ expect ( captureExceptionSpy ) . not . toHaveBeenCalled ( ) ;
715+ } ) ;
716+ } ) ;
569717} ) ;
0 commit comments