Skip to content

Commit 8b38c1f

Browse files
nicohrubecclaude
andcommitted
feat(core): Emit sentry.sdk.integrations on segment spans in span streaming
In the classic (non-streaming) pipeline, SDK integrations ride on the transaction event wrapper via `event.sdk.integrations`. The span streaming pipeline emits segment spans directly as span envelope items with no transaction wrapper, so that metadata never reaches ingest. Add `sentry.sdk.integrations` as a native array attribute on segment spans (matching the wire contract Relay expects: `type: 'array'`). Relies on the homogeneous-primitive-array serializer support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a71058e commit 8b38c1f

3 files changed

Lines changed: 94 additions & 0 deletions

File tree

packages/core/src/semanticAttributes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ export const SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID = 'sentry.segment.id';
5252
export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME = 'sentry.sdk.name';
5353
/** The version of the Sentry SDK */
5454
export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION = 'sentry.sdk.version';
55+
/** The list of integrations enabled in the Sentry SDK (e.g., ["InboundFilters", "BrowserTracing"]) */
56+
export const SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS = 'sentry.sdk.integrations';
5557

5658
/** The user ID (gated by sendDefaultPii) */
5759
export const SEMANTIC_ATTRIBUTE_USER_ID = 'user.id';

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { ScopeData } from '../../scope';
44
import {
55
SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT,
66
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
7+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
78
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
89
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
910
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
@@ -53,6 +54,7 @@ export function captureSpan(span: Span, client: Client): SerializedStreamedSpanW
5354

5455
if (spanJSON.is_segment) {
5556
applyScopeToSegmentSpan(spanJSON, finalScopeData);
57+
applySdkMetadataToSegmentSpan(spanJSON, client);
5658
// Allow hook subscribers to mutate the segment span JSON
5759
// This also invokes the `processSegmentSpan` hook of all integrations
5860
client.emit('processSegmentSpan', spanJSON);
@@ -90,6 +92,15 @@ function applyScopeToSegmentSpan(_segmentSpanJSON: StreamedSpanJSON, _scopeData:
9092
// This will follow in a separate PR
9193
}
9294

95+
function applySdkMetadataToSegmentSpan(segmentSpanJSON: StreamedSpanJSON, client: Client): void {
96+
const integrationNames = client.getOptions().integrations.map(i => i.name);
97+
if (!integrationNames.length) return;
98+
99+
safeSetSpanJSONAttributes(segmentSpanJSON, {
100+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: integrationNames,
101+
});
102+
}
103+
93104
function applyCommonSpanAttributes(
94105
spanJSON: StreamedSpanJSON,
95106
serializedSegmentSpan: StreamedSpanJSON,

packages/core/test/lib/tracing/spans/captureSpan.test.ts

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN,
88
SEMANTIC_ATTRIBUTE_SENTRY_RELEASE,
99
SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE,
10+
SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS,
1011
SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME,
1112
SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION,
1213
SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID,
@@ -291,6 +292,86 @@ describe('captureSpan', () => {
291292
});
292293
});
293294

295+
it('adds sentry.sdk.integrations to segment spans as an array attribute', () => {
296+
const client = new TestClient(
297+
getDefaultTestClientOptions({
298+
dsn: 'https://dsn@ingest.f00.f00/1',
299+
tracesSampleRate: 1,
300+
release: '1.0.0',
301+
environment: 'staging',
302+
integrations: [
303+
{ name: 'InboundFilters', setupOnce: () => {} },
304+
{ name: 'BrowserTracing', setupOnce: () => {} },
305+
],
306+
_metadata: {
307+
sdk: {
308+
name: 'sentry.javascript.browser',
309+
version: '9.0.0',
310+
},
311+
},
312+
}),
313+
);
314+
315+
const span = withScope(scope => {
316+
scope.setClient(client);
317+
const span = startInactiveSpan({ name: 'my-span', attributes: { 'sentry.op': 'http.client' } });
318+
span.end();
319+
return span;
320+
});
321+
322+
expect(captureSpan(span, client)).toStrictEqual({
323+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
324+
trace_id: expect.stringMatching(/^[\da-f]{32}$/),
325+
parent_span_id: undefined,
326+
links: undefined,
327+
start_timestamp: expect.any(Number),
328+
name: 'my-span',
329+
end_timestamp: expect.any(Number),
330+
status: 'ok',
331+
is_segment: true,
332+
attributes: {
333+
[SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'http.client' },
334+
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'manual' },
335+
[SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 },
336+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { value: 'my-span', type: 'string' },
337+
[SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { value: span.spanContext().spanId, type: 'string' },
338+
'sentry.span.source': { value: 'custom', type: 'string' },
339+
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { value: 'custom', type: 'string' },
340+
[SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { value: '1.0.0', type: 'string' },
341+
[SEMANTIC_ATTRIBUTE_SENTRY_ENVIRONMENT]: { value: 'staging', type: 'string' },
342+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { value: 'sentry.javascript.browser', type: 'string' },
343+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { value: '9.0.0', type: 'string' },
344+
[SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]: {
345+
type: 'array',
346+
value: ['InboundFilters', 'BrowserTracing'],
347+
},
348+
},
349+
_segmentSpan: span,
350+
});
351+
});
352+
353+
it('does not add sentry.sdk.integrations to non-segment child spans', () => {
354+
const client = new TestClient(
355+
getDefaultTestClientOptions({
356+
dsn: 'https://dsn@ingest.f00.f00/1',
357+
tracesSampleRate: 1,
358+
integrations: [{ name: 'InboundFilters', setupOnce: () => {} }],
359+
}),
360+
);
361+
362+
const serializedChild = withScope(scope => {
363+
scope.setClient(client);
364+
return startSpan({ name: 'segment' }, () => {
365+
const childSpan = startInactiveSpan({ name: 'child' });
366+
childSpan.end();
367+
return captureSpan(childSpan, client);
368+
});
369+
});
370+
371+
expect(serializedChild.is_segment).toBe(false);
372+
expect(serializedChild.attributes?.[SEMANTIC_ATTRIBUTE_SENTRY_SDK_INTEGRATIONS]).toBeUndefined();
373+
});
374+
294375
describe('client hooks', () => {
295376
it('calls processSpan and processSegmentSpan hooks for a segment span', () => {
296377
const client = new TestClient(

0 commit comments

Comments
 (0)