11import { describe , expect , it , vi } from 'vitest' ;
2- import type { StreamedSpanJSON } from '../../../../src' ;
2+ import type { Contexts , StreamedSpanJSON } from '../../../../src' ;
33import {
44 captureSpan ,
55 SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT ,
@@ -23,6 +23,7 @@ import {
2323 withStreamedSpan ,
2424} from '../../../../src' ;
2525import { inferSpanDataFromOtelAttributes , safeSetSpanJSONAttributes } from '../../../../src/tracing/spans/captureSpan' ;
26+ import { scopeContextsToSpanAttributes } from '../../../../src/tracing/spans/scopeContextAttributes' ;
2627import { getDefaultTestClientOptions , TestClient } from '../../../mocks/client' ;
2728
2829describe ( 'captureSpan' , ( ) => {
@@ -660,3 +661,201 @@ describe('inferSpanDataFromOtelAttributes', () => {
660661 expect ( spanJSON . name ) . toBe ( 'test' ) ;
661662 } ) ;
662663} ) ;
664+
665+ describe ( 'scopeContextsToSpanAttributes' , ( ) => {
666+ it ( 'returns empty object for empty contexts' , ( ) => {
667+ expect ( scopeContextsToSpanAttributes ( { } ) ) . toEqual ( { } ) ;
668+ } ) ;
669+
670+ it ( 'ignores unknown context names' , ( ) => {
671+ const contexts : Contexts = { my_custom_context : { foo : 'bar' } } ;
672+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( { } ) ;
673+ } ) ;
674+
675+ describe ( 'response context' , ( ) => {
676+ it ( 'maps status_code and body_size' , ( ) => {
677+ const contexts : Contexts = { response : { status_code : 200 , body_size : 1024 } } ;
678+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
679+ 'http.response.status_code' : 200 ,
680+ 'http.response.body.size' : 1024 ,
681+ } ) ;
682+ } ) ;
683+
684+ it ( 'omits missing fields' , ( ) => {
685+ const contexts : Contexts = { response : { status_code : 404 } } ;
686+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
687+ 'http.response.status_code' : 404 ,
688+ } ) ;
689+ } ) ;
690+ } ) ;
691+
692+ describe ( 'profile context' , ( ) => {
693+ it ( 'maps profile_id to sentry.profile_id' , ( ) => {
694+ const contexts : Contexts = { profile : { profile_id : 'abc123' } } ;
695+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
696+ 'sentry.profile_id' : 'abc123' ,
697+ } ) ;
698+ } ) ;
699+
700+ it ( 'maps profiler_id to sentry.profiler_id' , ( ) => {
701+ const contexts : Contexts = { profile : { profile_id : '' , profiler_id : 'prof-1' } } ;
702+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
703+ 'sentry.profiler_id' : 'prof-1' ,
704+ } ) ;
705+ } ) ;
706+
707+ it ( 'produces no attributes for empty profile context' , ( ) => {
708+ const contexts : Contexts = { profile : { profile_id : '' } } ;
709+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( { } ) ;
710+ } ) ;
711+ } ) ;
712+
713+ describe ( 'cloud_resource context' , ( ) => {
714+ it ( 'passes through dot-notation keys' , ( ) => {
715+ const contexts : Contexts = {
716+ cloud_resource : { 'cloud.provider' : 'cloudflare' , 'cloud.region' : 'us-east-1' } ,
717+ } ;
718+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
719+ 'cloud.provider' : 'cloudflare' ,
720+ 'cloud.region' : 'us-east-1' ,
721+ } ) ;
722+ } ) ;
723+
724+ it ( 'filters out null values' , ( ) => {
725+ const contexts : Contexts = {
726+ cloud_resource : { 'cloud.provider' : 'aws' , 'cloud.region' : undefined } ,
727+ } ;
728+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
729+ 'cloud.provider' : 'aws' ,
730+ } ) ;
731+ } ) ;
732+ } ) ;
733+
734+ describe ( 'culture context' , ( ) => {
735+ it ( 'maps locale and timezone' , ( ) => {
736+ const contexts : Contexts = { culture : { locale : 'en-US' , timezone : 'America/New_York' } } ;
737+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
738+ 'culture.locale' : 'en-US' ,
739+ 'culture.timezone' : 'America/New_York' ,
740+ } ) ;
741+ } ) ;
742+
743+ it ( 'omits missing fields' , ( ) => {
744+ const contexts : Contexts = { culture : { timezone : 'UTC' } } ;
745+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
746+ 'culture.timezone' : 'UTC' ,
747+ } ) ;
748+ } ) ;
749+ } ) ;
750+
751+ describe ( 'state context' , ( ) => {
752+ it ( 'maps state.type only' , ( ) => {
753+ const contexts : Contexts = {
754+ state : { state : { type : 'redux' , value : { counter : 42 , user : { name : 'test' } } } } ,
755+ } ;
756+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
757+ 'state.type' : 'redux' ,
758+ } ) ;
759+ } ) ;
760+
761+ it ( 'does not map state.value' , ( ) => {
762+ const contexts : Contexts = {
763+ state : { state : { type : 'pinia' , value : { items : [ 1 , 2 , 3 ] } } } ,
764+ } ;
765+ const attrs = scopeContextsToSpanAttributes ( contexts ) ;
766+ expect ( attrs ) . not . toHaveProperty ( 'state.value' ) ;
767+ expect ( attrs ) . not . toHaveProperty ( 'state.state.value' ) ;
768+ } ) ;
769+
770+ it ( 'handles missing state.state gracefully' , ( ) => {
771+ const contexts : Contexts = { state : { } as any } ;
772+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( { } ) ;
773+ } ) ;
774+ } ) ;
775+
776+ describe ( 'framework version contexts' , ( ) => {
777+ it ( 'maps angular.version' , ( ) => {
778+ const contexts : Contexts = { angular : { version : 17 } } ;
779+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
780+ 'angular.version' : 17 ,
781+ } ) ;
782+ } ) ;
783+
784+ it ( 'maps react.version' , ( ) => {
785+ const contexts : Contexts = { react : { version : '18.2.0' } } ;
786+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
787+ 'react.version' : '18.2.0' ,
788+ } ) ;
789+ } ) ;
790+ } ) ;
791+
792+ it ( 'maps multiple contexts at once' , ( ) => {
793+ const contexts : Contexts = {
794+ response : { status_code : 200 } ,
795+ culture : { timezone : 'UTC' } ,
796+ react : { version : '18.2.0' } ,
797+ } ;
798+ expect ( scopeContextsToSpanAttributes ( contexts ) ) . toEqual ( {
799+ 'http.response.status_code' : 200 ,
800+ 'culture.timezone' : 'UTC' ,
801+ 'react.version' : '18.2.0' ,
802+ } ) ;
803+ } ) ;
804+ } ) ;
805+
806+ describe ( 'applyScopeToSegmentSpan integration' , ( ) => {
807+ it ( 'applies scope contexts to segment span attributes' , ( ) => {
808+ const client = new TestClient (
809+ getDefaultTestClientOptions ( {
810+ dsn : 'https://dsn@ingest.f00.f00/1' ,
811+ tracesSampleRate : 1 ,
812+ release : '1.0.0' ,
813+ environment : 'production' ,
814+ } ) ,
815+ ) ;
816+
817+ const span = withScope ( scope => {
818+ scope . setClient ( client ) ;
819+ scope . setContext ( 'response' , { status_code : 201 } ) ;
820+ scope . setContext ( 'culture' , { timezone : 'Europe/Berlin' } ) ;
821+
822+ const span = startInactiveSpan ( { name : 'test-span' } ) ;
823+ span . end ( ) ;
824+ return span ;
825+ } ) ;
826+
827+ const serialized = captureSpan ( span , client ) ;
828+
829+ expect ( serialized . attributes ) . toEqual (
830+ expect . objectContaining ( {
831+ 'http.response.status_code' : { type : 'integer' , value : 201 } ,
832+ 'culture.timezone' : { type : 'string' , value : 'Europe/Berlin' } ,
833+ } ) ,
834+ ) ;
835+ } ) ;
836+
837+ it ( 'does not apply scope contexts to child spans' , ( ) => {
838+ const client = new TestClient (
839+ getDefaultTestClientOptions ( {
840+ dsn : 'https://dsn@ingest.f00.f00/1' ,
841+ tracesSampleRate : 1 ,
842+ release : '1.0.0' ,
843+ environment : 'production' ,
844+ } ) ,
845+ ) ;
846+
847+ const serializedChild = withScope ( scope => {
848+ scope . setClient ( client ) ;
849+ scope . setContext ( 'response' , { status_code : 200 } ) ;
850+
851+ return startSpan ( { name : 'segment' } , ( ) => {
852+ const childSpan = startInactiveSpan ( { name : 'child' } ) ;
853+ childSpan . end ( ) ;
854+ return captureSpan ( childSpan , client ) ;
855+ } ) ;
856+ } ) ;
857+
858+ expect ( serializedChild ?. is_segment ) . toBe ( false ) ;
859+ expect ( serializedChild ?. attributes ) . not . toHaveProperty ( 'http.response.status_code' ) ;
860+ } ) ;
861+ } ) ;
0 commit comments