@@ -80,19 +80,12 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW
8080 } ) ;
8181 }
8282
83- // Backfill sentry.op from span attributes when not explicitly set.
84- // OTel-originated spans don't have sentry.op set — we infer it from semantic conventions.
85- // The non-streamed path infers this in the SentrySpanExporter, but streamed spans skip the exporter.
86- if ( ! processedSpan . attributes ?. [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] ) {
87- // Access `kind` via duck-typing — OTel span objects have this property but it's not on Sentry's Span type
88- const spanKind = ( span as { kind ?: number } ) . kind ;
89- const inferredOp = inferOpFromAttributes ( processedSpan . attributes , spanKind ) ;
90- if ( inferredOp ) {
91- safeSetSpanJSONAttributes ( processedSpan , {
92- [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : inferredOp ,
93- } ) ;
94- }
95- }
83+ // Backfill span data from OTel semantic conventions when not explicitly set.
84+ // OTel-originated spans don't have sentry.op, description, etc. — the non-streamed path
85+ // infers these in the SentrySpanExporter, but streamed spans skip the exporter entirely.
86+ // Access `kind` via duck-typing — OTel span objects have this property but it's not on Sentry's Span type.
87+ const spanKind = ( span as { kind ?: number } ) . kind ;
88+ inferSpanDataFromOtelAttributes ( processedSpan , spanKind ) ;
9689
9790 return {
9891 ...streamedSpanJsonToSerializedSpan ( processedSpan ) ,
@@ -166,38 +159,68 @@ export function safeSetSpanJSONAttributes(
166159 } ) ;
167160}
168161
169- // OTel SpanKind values (we use the numeric values to avoid importing from @opentelemetry/api)
170- const SPAN_KIND_CLIENT = 2 ;
162+ // OTel SpanKind values (numeric to avoid importing from @opentelemetry/api)
171163const SPAN_KIND_SERVER = 1 ;
164+ const SPAN_KIND_CLIENT = 2 ;
172165
173166/**
174- * Infer `sentry.op` from span attributes and kind based on OTel semantic conventions.
175- * This is needed because OTel-originated spans don't set `sentry.op` — the non-streamed
176- * path infers it in the `SentrySpanExporter`, but streamed spans skip the exporter entirely.
167+ * Infer and backfill span data from OTel semantic conventions.
168+ * This mirrors what the `SentrySpanExporter` does for non-streamed spans via `getSpanData`/`inferSpanData`.
169+ * Streamed spans skip the exporter, so we do the inference here during capture.
170+ *
171+ * Uses `safeSetSpanJSONAttributes` so explicitly set attributes are never overwritten.
177172 */
178- function inferOpFromAttributes (
179- attributes ?: RawAttributes < Record < string , unknown > > ,
180- spanKind ?: number ,
181- ) : string | undefined {
173+ function inferSpanDataFromOtelAttributes ( spanJSON : StreamedSpanJSON , spanKind ?: number ) : void {
174+ const attributes = spanJSON . attributes ;
182175 if ( ! attributes ) {
183- return undefined ;
176+ return ;
184177 }
185178
186179 const httpMethod = attributes [ 'http.request.method' ] || attributes [ 'http.method' ] ;
187180 if ( httpMethod ) {
181+ const opParts = [ 'http' ] ;
188182 if ( spanKind === SPAN_KIND_CLIENT ) {
189- return 'http.client' ;
183+ opParts . push ( 'client' ) ;
184+ } else if ( spanKind === SPAN_KIND_SERVER ) {
185+ opParts . push ( 'server' ) ;
190186 }
191- if ( spanKind === SPAN_KIND_SERVER ) {
192- return 'http.server' ;
187+
188+ if ( attributes [ 'sentry.http.prefetch' ] ) {
189+ opParts . push ( 'prefetch' ) ;
193190 }
194- return 'http' ;
191+
192+ safeSetSpanJSONAttributes ( spanJSON , {
193+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : opParts . join ( '.' ) ,
194+ } ) ;
195+ return ;
195196 }
196197
197198 const dbSystem = attributes [ 'db.system.name' ] || attributes [ 'db.system' ] ;
198199 if ( dbSystem ) {
199- return 'db' ;
200+ safeSetSpanJSONAttributes ( spanJSON , {
201+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'db' ,
202+ } ) ;
203+ return ;
200204 }
201205
202- return undefined ;
206+ if ( attributes [ 'rpc.service' ] ) {
207+ safeSetSpanJSONAttributes ( spanJSON , {
208+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'rpc' ,
209+ } ) ;
210+ return ;
211+ }
212+
213+ if ( attributes [ 'messaging.system' ] ) {
214+ safeSetSpanJSONAttributes ( spanJSON , {
215+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : 'message' ,
216+ } ) ;
217+ return ;
218+ }
219+
220+ const faasTrigger = attributes [ 'faas.trigger' ] ;
221+ if ( faasTrigger ) {
222+ safeSetSpanJSONAttributes ( spanJSON , {
223+ [ SEMANTIC_ATTRIBUTE_SENTRY_OP ] : `${ faasTrigger } ` ,
224+ } ) ;
225+ }
203226}
0 commit comments