Skip to content

Commit 49564b3

Browse files
committed
backfill all ops
1 parent f23e7d1 commit 49564b3

4 files changed

Lines changed: 109 additions & 29 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
traceLifecycle: 'stream',
10+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/node';
2+
import { startExpressServerAndSendPortToRunner } from '@sentry-internal/node-integration-tests';
3+
import cors from 'cors';
4+
import express from 'express';
5+
6+
const app = express();
7+
8+
app.use(cors());
9+
10+
app.get('/test', (_req, res) => {
11+
res.send({ response: 'ok' });
12+
});
13+
14+
Sentry.setupExpressErrorHandler(app);
15+
16+
startExpressServerAndSendPortToRunner(app);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { afterAll, describe, expect } from 'vitest';
2+
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';
3+
4+
describe('httpIntegration-streamed', () => {
5+
afterAll(() => {
6+
cleanupChildProcesses();
7+
});
8+
9+
createEsmAndCjsTests(__dirname, 'server.mjs', 'instrument.mjs', (createRunner, test) => {
10+
test('infers sentry.op http.server on streamed server spans', async () => {
11+
const runner = createRunner()
12+
.expect({
13+
span: container => {
14+
const serverSpan = container.items.find(
15+
item =>
16+
item.attributes?.['sentry.op']?.type === 'string' &&
17+
item.attributes['sentry.op'].value === 'http.server',
18+
);
19+
20+
expect(serverSpan).toBeDefined();
21+
expect(serverSpan?.is_segment).toBe(true);
22+
},
23+
})
24+
.start();
25+
26+
await runner.makeRequest('get', '/test');
27+
28+
await runner.completed();
29+
});
30+
});
31+
});

packages/core/src/tracing/spans/captureSpan.ts

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -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)
171163
const 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

Comments
 (0)