@@ -21,7 +21,7 @@ import {
2121 withScope ,
2222 withStreamedSpan ,
2323} from '../../../../src' ;
24- import { safeSetSpanJSONAttributes } from '../../../../src/tracing/spans/captureSpan' ;
24+ import { inferSpanDataFromOtelAttributes , safeSetSpanJSONAttributes } from '../../../../src/tracing/spans/captureSpan' ;
2525import { getDefaultTestClientOptions , TestClient } from '../../../mocks/client' ;
2626
2727describe ( 'captureSpan' , ( ) => {
@@ -483,3 +483,157 @@ describe('safeSetSpanJSONAttributes', () => {
483483 expect ( spanJSON . attributes ) . toEqual ( { } ) ;
484484 } ) ;
485485} ) ;
486+
487+ describe ( 'inferSpanDataFromOtelAttributes' , ( ) => {
488+ function makeSpanJSON ( name : string , attributes : Record < string , unknown > ) : StreamedSpanJSON {
489+ return {
490+ name,
491+ span_id : 'abc123' ,
492+ trace_id : 'def456' ,
493+ start_timestamp : 0 ,
494+ end_timestamp : 1 ,
495+ status : 'ok' ,
496+ is_segment : false ,
497+ attributes,
498+ } ;
499+ }
500+
501+ describe ( 'http spans' , ( ) => {
502+ it ( 'infers http.client op for CLIENT kind' , ( ) => {
503+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' } ) ;
504+ inferSpanDataFromOtelAttributes ( spanJSON , 2 ) ; // SPAN_KIND_CLIENT
505+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http.client' ) ;
506+ } ) ;
507+
508+ it ( 'infers http.server op for SERVER kind' , ( ) => {
509+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' } ) ;
510+ inferSpanDataFromOtelAttributes ( spanJSON , 1 ) ; // SPAN_KIND_SERVER
511+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http.server' ) ;
512+ } ) ;
513+
514+ it ( 'infers http op when kind is unknown' , ( ) => {
515+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' } ) ;
516+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
517+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http' ) ;
518+ } ) ;
519+
520+ it ( 'appends prefetch to op' , ( ) => {
521+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' , 'sentry.http.prefetch' : true } ) ;
522+ inferSpanDataFromOtelAttributes ( spanJSON , 2 ) ;
523+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http.client.prefetch' ) ;
524+ } ) ;
525+
526+ it ( 'sets name and source from http.route' , ( ) => {
527+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' , 'http.route' : '/users/:id' } ) ;
528+ inferSpanDataFromOtelAttributes ( spanJSON , 1 ) ;
529+ expect ( spanJSON . name ) . toBe ( 'GET /users/:id' ) ;
530+ expect ( spanJSON . attributes ?. [ 'sentry.source' ] ) . toBe ( 'route' ) ;
531+ } ) ;
532+
533+ it ( 'does not overwrite name when no http.route' , ( ) => {
534+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' , 'url.full' : 'http://example.com/api' } ) ;
535+ inferSpanDataFromOtelAttributes ( spanJSON , 2 ) ;
536+ expect ( spanJSON . name ) . toBe ( 'GET' ) ;
537+ } ) ;
538+
539+ it ( 'does not overwrite sentry.op if already set' , ( ) => {
540+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.request.method' : 'GET' , 'sentry.op' : 'http.client.custom' } ) ;
541+ inferSpanDataFromOtelAttributes ( spanJSON , 2 ) ;
542+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http.client.custom' ) ;
543+ } ) ;
544+
545+ it ( 'restores custom span name from sentry.custom_span_name' , ( ) => {
546+ const spanJSON = makeSpanJSON ( 'overwritten-by-otel' , {
547+ 'http.request.method' : 'GET' ,
548+ 'sentry.custom_span_name' : 'my-custom-name' ,
549+ 'sentry.source' : 'custom' ,
550+ 'http.route' : '/users/:id' ,
551+ } ) ;
552+ inferSpanDataFromOtelAttributes ( spanJSON , 1 ) ;
553+ expect ( spanJSON . name ) . toBe ( 'my-custom-name' ) ;
554+ } ) ;
555+
556+ it ( 'does not overwrite name when sentry.source is custom' , ( ) => {
557+ const spanJSON = makeSpanJSON ( 'my-name' , {
558+ 'http.request.method' : 'GET' ,
559+ 'sentry.source' : 'custom' ,
560+ 'http.route' : '/users/:id' ,
561+ } ) ;
562+ inferSpanDataFromOtelAttributes ( spanJSON , 1 ) ;
563+ expect ( spanJSON . name ) . toBe ( 'my-name' ) ;
564+ } ) ;
565+
566+ it ( 'supports legacy http.method attribute' , ( ) => {
567+ const spanJSON = makeSpanJSON ( 'GET' , { 'http.method' : 'GET' } ) ;
568+ inferSpanDataFromOtelAttributes ( spanJSON , 2 ) ;
569+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http.client' ) ;
570+ } ) ;
571+ } ) ;
572+
573+ describe ( 'db spans' , ( ) => {
574+ it ( 'infers db op' , ( ) => {
575+ const spanJSON = makeSpanJSON ( 'redis' , { 'db.system' : 'redis' } ) ;
576+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
577+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'db' ) ;
578+ } ) ;
579+
580+ it ( 'sets name from db.statement' , ( ) => {
581+ const spanJSON = makeSpanJSON ( 'mysql' , { 'db.system' : 'mysql' , 'db.statement' : 'SELECT * FROM users' } ) ;
582+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
583+ expect ( spanJSON . name ) . toBe ( 'SELECT * FROM users' ) ;
584+ expect ( spanJSON . attributes ?. [ 'sentry.source' ] ) . toBe ( 'task' ) ;
585+ } ) ;
586+
587+ it ( 'skips db inference for cache spans' , ( ) => {
588+ const spanJSON = makeSpanJSON ( 'cache-get' , { 'db.system' : 'redis' , 'sentry.op' : 'cache.get_item' } ) ;
589+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
590+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'cache.get_item' ) ;
591+ expect ( spanJSON . name ) . toBe ( 'cache-get' ) ;
592+ } ) ;
593+
594+ it ( 'restores custom span name from sentry.custom_span_name' , ( ) => {
595+ const spanJSON = makeSpanJSON ( 'overwritten' , {
596+ 'db.system' : 'mysql' ,
597+ 'db.statement' : 'SELECT 1' ,
598+ 'sentry.custom_span_name' : 'my-db-span' ,
599+ 'sentry.source' : 'custom' ,
600+ } ) ;
601+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
602+ expect ( spanJSON . name ) . toBe ( 'my-db-span' ) ;
603+ } ) ;
604+ } ) ;
605+
606+ describe ( 'other span types' , ( ) => {
607+ it ( 'infers rpc op' , ( ) => {
608+ const spanJSON = makeSpanJSON ( 'grpc' , { 'rpc.service' : 'UserService' } ) ;
609+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
610+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'rpc' ) ;
611+ } ) ;
612+
613+ it ( 'infers message op' , ( ) => {
614+ const spanJSON = makeSpanJSON ( 'kafka' , { 'messaging.system' : 'kafka' } ) ;
615+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
616+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'message' ) ;
617+ } ) ;
618+
619+ it ( 'infers faas op from trigger' , ( ) => {
620+ const spanJSON = makeSpanJSON ( 'lambda' , { 'faas.trigger' : 'http' } ) ;
621+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
622+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBe ( 'http' ) ;
623+ } ) ;
624+ } ) ;
625+
626+ it ( 'does nothing when attributes are missing' , ( ) => {
627+ const spanJSON = makeSpanJSON ( 'test' , undefined as unknown as Record < string , unknown > ) ;
628+ spanJSON . attributes = undefined ;
629+ inferSpanDataFromOtelAttributes ( spanJSON , 2 ) ;
630+ expect ( spanJSON . attributes ) . toBeUndefined ( ) ;
631+ } ) ;
632+
633+ it ( 'does nothing for spans without recognizable attributes' , ( ) => {
634+ const spanJSON = makeSpanJSON ( 'test' , { 'custom.attr' : 'value' } ) ;
635+ inferSpanDataFromOtelAttributes ( spanJSON ) ;
636+ expect ( spanJSON . attributes ?. [ 'sentry.op' ] ) . toBeUndefined ( ) ;
637+ expect ( spanJSON . name ) . toBe ( 'test' ) ;
638+ } ) ;
639+ } ) ;
0 commit comments