@@ -2,6 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
22import type { Contexts , SerializedStreamedSpan , SpanJSON , StreamedSpanJSON } from '../../../../src' ;
33import {
44 captureSpan ,
5+ debug ,
56 SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT ,
67 SEMANTIC_ATTRIBUTE_SENTRY_OP ,
78 SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN ,
@@ -13,6 +14,7 @@ import {
1314 SEMANTIC_ATTRIBUTE_USER_ID ,
1415 SEMANTIC_ATTRIBUTE_USER_IP_ADDRESS ,
1516 SEMANTIC_ATTRIBUTE_USER_USERNAME ,
17+ spanStreamingIntegration ,
1618 startInactiveSpan ,
1719 startSpan ,
1820 withStaticSpan ,
@@ -547,6 +549,70 @@ describe('captureSpan', () => {
547549 consoleWarnSpy . mockRestore ( ) ;
548550 } ) ;
549551
552+ it ( 'keeps the span and logs an error if the beforeSendSpan callback throws' , ( ) => {
553+ const debugErrorSpy = vi . spyOn ( debug , 'error' ) . mockImplementation ( ( ) => undefined ) ;
554+ const error = new Error ( 'beforeSendSpan is broken' ) ;
555+ // A v10 callback that was not migrated to the streamed format throws like this, because
556+ // `data` doesn't exist on a `StreamedSpanJSON`.
557+ const beforeSendSpan = vi . fn ( ( ) => {
558+ throw error ;
559+ } ) ;
560+
561+ const client = new TestClient (
562+ getDefaultTestClientOptions ( {
563+ dsn : 'https://dsn@ingest.f00.f00/1' ,
564+ tracesSampleRate : 1 ,
565+ traceLifecycle : 'stream' ,
566+ beforeSendSpan : beforeSendSpan as unknown as TestClientOptions [ 'beforeSendSpan' ] ,
567+ } ) ,
568+ ) ;
569+
570+ const span = withScope ( scope => {
571+ scope . setClient ( client ) ;
572+ const span = startInactiveSpan ( { name : 'my-span' , attributes : { 'sentry.op' : 'http.client' } } ) ;
573+ span . end ( ) ;
574+ return span ;
575+ } ) ;
576+
577+ const serialized = captureSpan ( span , client ) ;
578+
579+ expect ( serialized . name ) . toBe ( 'my-span' ) ;
580+ expect ( serialized . attributes [ 'sentry.op' ] ) . toEqual ( { type : 'string' , value : 'http.client' } ) ;
581+ expect ( debugErrorSpy ) . toHaveBeenCalledWith (
582+ 'The `beforeSendSpan` callback threw an error, sending the span unmodified:' ,
583+ error ,
584+ ) ;
585+
586+ debugErrorSpy . mockRestore ( ) ;
587+ } ) ;
588+
589+ it ( "doesn't let a throwing beforeSendSpan callback propagate out of span.end()" , ( ) => {
590+ const debugErrorSpy = vi . spyOn ( debug , 'error' ) . mockImplementation ( ( ) => undefined ) ;
591+ const client = new TestClient (
592+ getDefaultTestClientOptions ( {
593+ dsn : 'https://dsn@ingest.f00.f00/1' ,
594+ tracesSampleRate : 1 ,
595+ traceLifecycle : 'stream' ,
596+ integrations : [ spanStreamingIntegration ( ) ] ,
597+ beforeSendSpan : ( ( ) => {
598+ throw new Error ( 'beforeSendSpan is broken' ) ;
599+ } ) as unknown as TestClientOptions [ 'beforeSendSpan' ] ,
600+ } ) ,
601+ ) ;
602+
603+ // Spans are captured synchronously from the `afterSpanEnd` hook, so a throwing callback
604+ // would otherwise surface in user code that ended the span.
605+ expect ( ( ) =>
606+ withScope ( scope => {
607+ scope . setClient ( client ) ;
608+ client . init ( ) ;
609+ startSpan ( { name : 'my-span' } , ( ) => undefined ) ;
610+ } ) ,
611+ ) . not . toThrow ( ) ;
612+
613+ debugErrorSpy . mockRestore ( ) ;
614+ } ) ;
615+
550616 // Standalone spans (INP web vital spans) are streamed even with the static trace lifecycle,
551617 // so a `withStaticSpan` callback has to see them in the v1 format it expects.
552618 describe ( 'with the static trace lifecycle' , ( ) => {
0 commit comments