From 58b1179d3243d539fc6bd3428dddc758ae11c4d5 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Mar 2026 15:43:02 +0100 Subject: [PATCH 1/8] test(node): Add span streaming integration tests --- .../public-api/startSpan/streamed/scenario.ts | 28 ++++++ .../public-api/startSpan/streamed/test.ts | 92 +++++++++++++++++++ .../utils/assertions.ts | 22 +++++ .../utils/runner.ts | 29 +++++- .../public-api/startSpan/streamed/scenario.ts | 25 +++++ .../public-api/startSpan/streamed/test.ts | 92 +++++++++++++++++++ .../utils/assertions.ts | 22 +++++ .../node-integration-tests/utils/runner.ts | 29 +++++- packages/core/src/index.ts | 1 + 9 files changed, 336 insertions(+), 4 deletions(-) create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts new file mode 100644 index 000000000000..49bbfb9415f5 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts @@ -0,0 +1,28 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + integrations: [Sentry.spanStreamingIntegration()], + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { + // noop + }); + + const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' }); + inactiveSpan.end(); + + Sentry.startSpanManual({ name: 'test-manual-span' }, span => { + span.end(); + }); +}); + +void Sentry.flush(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts new file mode 100644 index 000000000000..f55684729a17 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -0,0 +1,92 @@ +import { SDK_VERSION } from '@sentry/core'; +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('sends a streamed span envelope with correct envelope header', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + span: { + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node-core', + version: SDK_VERSION, + }, + trace: expect.objectContaining({ + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + transaction: 'test-span', + }), + }, + }) + .start() + .completed(); +}); + +test('sends a streamed span envelope with correct spans for a manually started span with children', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: container => { + const spans = container.items; + expect(spans.length).toBe(4); + + const segmentSpan = spans.find(s => !!s.is_segment); + expect(segmentSpan).toBeDefined(); + + const segmentSpanId = segmentSpan!.span_id; + const traceId = segmentSpan!.trace_id; + + const childSpan = spans.find(s => s.name === 'test-child-span'); + expect(childSpan).toBeDefined(); + expect(childSpan).toMatchObject({ + name: 'test-child-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); + expect(inactiveSpan).toBeDefined(); + expect(inactiveSpan).toMatchObject({ + name: 'test-inactive-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + const manualSpan = spans.find(s => s.name === 'test-manual-span'); + expect(manualSpan).toBeDefined(); + expect(manualSpan).toMatchObject({ + name: 'test-manual-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + expect(segmentSpan).toMatchObject({ + name: 'test-span', + is_segment: true, + trace_id: traceId, + span_id: segmentSpanId, + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/utils/assertions.ts b/dev-packages/node-core-integration-tests/utils/assertions.ts index 00c6019fab0c..0816e894118b 100644 --- a/dev-packages/node-core-integration-tests/utils/assertions.ts +++ b/dev-packages/node-core-integration-tests/utils/assertions.ts @@ -6,6 +6,7 @@ import type { SerializedLogContainer, SerializedMetricContainer, SerializedSession, + SerializedStreamedSpanContainer, SessionAggregates, TransactionEvent, } from '@sentry/core'; @@ -86,6 +87,16 @@ export function assertSentryMetricContainer( }); } +export function assertSentrySpanContainer( + actual: SerializedStreamedSpanContainer, + expected: Partial, +): void { + expect(actual).toMatchObject({ + items: expect.any(Array), + ...expected, + }); +} + export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { expect(actual).toEqual({ event_id: expect.any(String), @@ -97,3 +108,14 @@ export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { + expect(actual).toEqual({ + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node-core', + version: SDK_VERSION, + }, + ...expected, + }); +} diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts index 416d55d803a1..83d2069cb7cb 100644 --- a/dev-packages/node-core-integration-tests/utils/runner.ts +++ b/dev-packages/node-core-integration-tests/utils/runner.ts @@ -9,6 +9,7 @@ import type { SerializedLogContainer, SerializedMetricContainer, SerializedSession, + SerializedStreamedSpanContainer, SessionAggregates, TransactionEvent, } from '@sentry/core'; @@ -26,7 +27,9 @@ import { assertSentryMetricContainer, assertSentrySession, assertSentrySessions, + assertSentrySpanContainer, assertSentryTransaction, + assertSpanEnvelopeHeader, } from './assertions'; import { createBasicSentryServer } from './server'; @@ -125,6 +128,9 @@ type ExpectedCheckIn = Partial | ((event: SerializedCheckIn) type ExpectedClientReport = Partial | ((event: ClientReport) => void); type ExpectedLogContainer = Partial | ((event: SerializedLogContainer) => void); type ExpectedMetricContainer = Partial | ((event: SerializedMetricContainer) => void); +type ExpectedSpanContainer = + | Partial + | ((container: SerializedStreamedSpanContainer) => void); type Expected = | { @@ -150,6 +156,9 @@ type Expected = } | { trace_metric: ExpectedMetricContainer; + } + | { + span: ExpectedSpanContainer; }; type ExpectedEnvelopeHeader = @@ -157,7 +166,8 @@ type ExpectedEnvelopeHeader = | { transaction: Partial } | { session: Partial } | { sessions: Partial } - | { log: Partial }; + | { log: Partial } + | { span: Partial }; type StartResult = { completed(): Promise; @@ -360,7 +370,11 @@ export function createRunner(...paths: string[]) { return; } - assertEnvelopeHeader(header, expected); + if (envelopeItemType === 'span') { + assertSpanEnvelopeHeader(header, expected); + } else { + assertEnvelopeHeader(header, expected); + } expectCallbackCalled(); } catch (e) { @@ -412,6 +426,9 @@ export function createRunner(...paths: string[]) { } else if ('trace_metric' in expected) { expectMetric(item[1] as SerializedMetricContainer, expected.trace_metric); expectCallbackCalled(); + } else if ('span' in expected) { + expectSpanContainer(item[1] as SerializedStreamedSpanContainer, expected.span); + expectCallbackCalled(); } else { throw new Error( `Unhandled expected envelope item type: ${JSON.stringify(expected)}\nItem: ${JSON.stringify(item)}`, @@ -666,6 +683,14 @@ function expectMetric(item: SerializedMetricContainer, expected: ExpectedMetricC } } +function expectSpanContainer(item: SerializedStreamedSpanContainer, expected: ExpectedSpanContainer): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySpanContainer(item, expected); + } +} + /** * Converts ESM import statements to CommonJS require statements * @param content The content of an ESM file diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts new file mode 100644 index 000000000000..49cb7a633ef5 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts @@ -0,0 +1,25 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + integrations: [Sentry.spanStreamingIntegration()], + transport: loggingTransport, +}); + +Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { + Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { + // noop + }); + + const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' }); + inactiveSpan.end(); + + Sentry.startSpanManual({ name: 'test-manual-span' }, span => { + span.end(); + }); +}); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts new file mode 100644 index 000000000000..87d7021929a1 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts @@ -0,0 +1,92 @@ +import { SDK_VERSION } from '@sentry/core'; +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('sends a streamed span envelope with correct envelope header', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + span: { + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node', + version: SDK_VERSION, + }, + trace: expect.objectContaining({ + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + transaction: 'test-span', + }), + }, + }) + .start() + .completed(); +}); + +test('sends a streamed span envelope with correct spans for a manually started span with children', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: container => { + const spans = container.items; + expect(spans.length).toBe(4); + + const segmentSpan = spans.find(s => !!s.is_segment); + expect(segmentSpan).toBeDefined(); + + const segmentSpanId = segmentSpan!.span_id; + const traceId = segmentSpan!.trace_id; + + const childSpan = spans.find(s => s.name === 'test-child-span'); + expect(childSpan).toBeDefined(); + expect(childSpan).toMatchObject({ + name: 'test-child-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); + expect(inactiveSpan).toBeDefined(); + expect(inactiveSpan).toMatchObject({ + name: 'test-inactive-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + const manualSpan = spans.find(s => s.name === 'test-manual-span'); + expect(manualSpan).toBeDefined(); + expect(manualSpan).toMatchObject({ + name: 'test-manual-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + expect(segmentSpan).toMatchObject({ + name: 'test-span', + is_segment: true, + trace_id: traceId, + span_id: segmentSpanId, + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/utils/assertions.ts b/dev-packages/node-integration-tests/utils/assertions.ts index 8d9fb5f2251f..8b78ddda0556 100644 --- a/dev-packages/node-integration-tests/utils/assertions.ts +++ b/dev-packages/node-integration-tests/utils/assertions.ts @@ -6,6 +6,7 @@ import type { SerializedLogContainer, SerializedMetricContainer, SerializedSession, + SerializedStreamedSpanContainer, SessionAggregates, TransactionEvent, } from '@sentry/core'; @@ -86,6 +87,16 @@ export function assertSentryMetricContainer( }); } +export function assertSentrySpanContainer( + actual: SerializedStreamedSpanContainer, + expected: Partial, +): void { + expect(actual).toMatchObject({ + items: expect.any(Array), + ...expected, + }); +} + export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { expect(actual).toEqual({ event_id: expect.any(String), @@ -97,3 +108,14 @@ export function assertEnvelopeHeader(actual: Envelope[0], expected: Partial): void { + expect(actual).toEqual({ + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node', + version: SDK_VERSION, + }, + ...expected, + }); +} diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index ee2fae0bc06b..bd81cecdf7f9 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -9,6 +9,7 @@ import type { SerializedLogContainer, SerializedMetricContainer, SerializedSession, + SerializedStreamedSpanContainer, SessionAggregates, TransactionEvent, } from '@sentry/core'; @@ -29,7 +30,9 @@ import { assertSentryMetricContainer, assertSentrySession, assertSentrySessions, + assertSentrySpanContainer, assertSentryTransaction, + assertSpanEnvelopeHeader, } from './assertions'; const execPromise = promisify(exec); @@ -135,6 +138,9 @@ type ExpectedCheckIn = Partial | ((event: SerializedCheckIn) type ExpectedClientReport = Partial | ((event: ClientReport) => void); type ExpectedLogContainer = Partial | ((event: SerializedLogContainer) => void); type ExpectedMetricContainer = Partial | ((event: SerializedMetricContainer) => void); +type ExpectedSpanContainer = + | Partial + | ((container: SerializedStreamedSpanContainer) => void); type Expected = | { @@ -160,6 +166,9 @@ type Expected = } | { trace_metric: ExpectedMetricContainer; + } + | { + span: ExpectedSpanContainer; }; type ExpectedEnvelopeHeader = @@ -167,7 +176,8 @@ type ExpectedEnvelopeHeader = | { transaction: Partial } | { session: Partial } | { sessions: Partial } - | { log: Partial }; + | { log: Partial } + | { span: Partial }; type StartResult = { completed(): Promise; @@ -479,7 +489,11 @@ export function createRunner(...paths: string[]) { return; } - assertEnvelopeHeader(header, expected); + if (envelopeItemType === 'span') { + assertSpanEnvelopeHeader(header, expected); + } else { + assertEnvelopeHeader(header, expected); + } expectCallbackCalled(); } catch (e) { @@ -531,6 +545,9 @@ export function createRunner(...paths: string[]) { } else if ('trace_metric' in expected) { expectMetric(item[1] as SerializedMetricContainer, expected.trace_metric); expectCallbackCalled(); + } else if ('span' in expected) { + expectSpanContainer(item[1] as SerializedStreamedSpanContainer, expected.span); + expectCallbackCalled(); } else { throw new Error( `Unhandled expected envelope item type: ${JSON.stringify(expected)}\nItem: ${JSON.stringify(item)}`, @@ -797,6 +814,14 @@ function expectMetric(item: SerializedMetricContainer, expected: ExpectedMetricC } } +function expectSpanContainer(item: SerializedStreamedSpanContainer, expected: ExpectedSpanContainer): void { + if (typeof expected === 'function') { + expected(item); + } else { + assertSentrySpanContainer(item, expected); + } +} + /** * Converts ESM import statements to CommonJS require statements * @param content The content of an ESM file diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 539405f09adc..3b7b7d4bbd30 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -469,6 +469,7 @@ export type { SpanContextData, TraceFlag, SerializedStreamedSpan, + SerializedStreamedSpanContainer, StreamedSpanJSON, } from './types-hoist/span'; export type { SpanStatus } from './types-hoist/spanStatus'; From e442e39bf6d2df74eb8891e7956099746f24d22e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Mar 2026 16:24:05 +0100 Subject: [PATCH 2/8] add startSpan API and relationship tests --- .../scenario.ts | 3 +- .../test.ts | 61 +++++++++++++++++-- .../parallel-root-spans-streamed/scenario.ts | 36 +++++++++++ .../parallel-root-spans-streamed/test.ts | 31 ++++++++++ .../scenario.ts | 32 ++++++++++ .../parallel-spans-in-scope-streamed/test.ts | 29 +++++++++ .../scenario.ts | 38 ++++++++++++ .../test.ts | 29 +++++++++ .../utils/runner.ts | 8 ++- .../parallel-root-spans-streamed/scenario.ts | 33 ++++++++++ .../parallel-root-spans-streamed/test.ts | 31 ++++++++++ .../scenario.ts | 29 +++++++++ .../parallel-spans-in-scope-streamed/test.ts | 29 +++++++++ .../scenario.ts | 35 +++++++++++ .../test.ts | 29 +++++++++ .../node-integration-tests/utils/runner.ts | 8 ++- packages/core/src/utils/spanUtils.ts | 3 +- 17 files changed, 454 insertions(+), 10 deletions(-) rename dev-packages/node-core-integration-tests/suites/public-api/startSpan/{streamed => basic-usage-streamed}/scenario.ts (79%) rename dev-packages/node-core-integration-tests/suites/public-api/startSpan/{streamed => basic-usage-streamed}/test.ts (50%) create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts similarity index 79% rename from dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts rename to dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts index 49bbfb9415f5..46d9f4005e6b 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/scenario.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts @@ -12,12 +12,13 @@ const client = Sentry.init({ setupOtel(client); -Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { +Sentry.startSpan({ name: 'test-span', op: 'test' }, segmentSpan => { Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { // noop }); const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' }); + inactiveSpan.addLink({ context: segmentSpan.spanContext(), attributes: { 'sentry.link.type': 'some_relation' } }); inactiveSpan.end(); Sentry.startSpanManual({ name: 'test-manual-span' }, span => { diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts similarity index 50% rename from dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts rename to dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index f55684729a17..c7682cc8792c 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -1,4 +1,12 @@ -import { SDK_VERSION } from '@sentry/core'; +import { + SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, + SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME, +} from '@sentry/core'; import { expect, test } from 'vitest'; import { createRunner } from '../../../../utils/runner'; @@ -39,7 +47,17 @@ test('sends a streamed span envelope with correct spans for a manually started s const childSpan = spans.find(s => s.name === 'test-child-span'); expect(childSpan).toBeDefined(); - expect(childSpan).toMatchObject({ + expect(childSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + type: 'string', + value: 'test-child', + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + }, name: 'test-child-span', is_segment: false, parent_span_id: segmentSpanId, @@ -52,7 +70,26 @@ test('sends a streamed span envelope with correct spans for a manually started s const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); expect(inactiveSpan).toBeDefined(); - expect(inactiveSpan).toMatchObject({ + expect(inactiveSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + }, + links: [ + { + attributes: { + 'sentry.link.type': { + type: 'string', + value: 'some_relation', + }, + }, + sampled: true, + span_id: segmentSpanId, + trace_id: traceId, + }, + ], name: 'test-inactive-span', is_segment: false, parent_span_id: segmentSpanId, @@ -65,7 +102,13 @@ test('sends a streamed span envelope with correct spans for a manually started s const manualSpan = spans.find(s => s.name === 'test-manual-span'); expect(manualSpan).toBeDefined(); - expect(manualSpan).toMatchObject({ + expect(manualSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + }, name: 'test-manual-span', is_segment: false, parent_span_id: segmentSpanId, @@ -76,7 +119,15 @@ test('sends a streamed span envelope with correct spans for a manually started s status: 'ok', }); - expect(segmentSpan).toMatchObject({ + expect(segmentSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + }, name: 'test-span', is_segment: true, trace_id: traceId, diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts new file mode 100644 index 000000000000..048b06f29178 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts @@ -0,0 +1,36 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), +}); + +const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, +); + +Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, +); + +Sentry.flush(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts new file mode 100644 index 000000000000..5f408fe9eac7 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts @@ -0,0 +1,31 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends manually started streamed parallel root spans in root context', async () => { + expect.assertions(7); + + await createRunner(__dirname, 'scenario.ts') + .expect({ span: { items: [{ name: 'test_span_1' }] } }) + .expect({ + span: spanContainer => { + expect(spanContainer).toBeDefined(); + const traceId = spanContainer.items[0]!.trace_id; + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + + // It ignores propagation context of the root context + expect(traceId).not.toBe('12345678901234567890123456789012'); + expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); + + // Different trace ID than the first span + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts new file mode 100644 index 000000000000..fd283637b0b6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts @@ -0,0 +1,32 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.withScope(() => { + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); + +Sentry.flush(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts new file mode 100644 index 000000000000..a9eb9805b3d2 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts @@ -0,0 +1,29 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends manually started streamed parallel root spans outside of root context', async () => { + expect.assertions(6); + + await createRunner(__dirname, 'scenario.ts') + .expect({ span: { items: [{ name: 'test_span_1' }] } }) + .expect({ + span: spanContainer => { + expect(spanContainer).toBeDefined(); + const traceId = spanContainer.items[0]!.trace_id; + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); + + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts new file mode 100644 index 000000000000..367ce6eda6fa --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts @@ -0,0 +1,38 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-core-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.withScope(scope => { + scope.setPropagationContext({ + parentSpanId: '1234567890123456', + traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), + }); + + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); + +Sentry.flush(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts new file mode 100644 index 000000000000..1346cb9e20fd --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts @@ -0,0 +1,29 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends manually started streamed parallel root spans outside of root context with parentSpanId', async () => { + expect.assertions(6); + + await createRunner(__dirname, 'scenario.ts') + .expect({ span: { items: [{ name: 'test_span_1' }] } }) + .expect({ + span: spanContainer => { + expect(spanContainer).toBeDefined(); + const traceId = spanContainer.items[0]!.trace_id; + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); + + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts index 83d2069cb7cb..f40ea4fb1c65 100644 --- a/dev-packages/node-core-integration-tests/utils/runner.ts +++ b/dev-packages/node-core-integration-tests/utils/runner.ts @@ -120,6 +120,12 @@ async function runDockerCompose(options: DockerOptions): Promise { }); } +type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + type ExpectedEvent = Partial | ((event: Event) => void); type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); type ExpectedSession = Partial | ((event: SerializedSession) => void); @@ -129,7 +135,7 @@ type ExpectedClientReport = Partial | ((event: ClientReport) => vo type ExpectedLogContainer = Partial | ((event: SerializedLogContainer) => void); type ExpectedMetricContainer = Partial | ((event: SerializedMetricContainer) => void); type ExpectedSpanContainer = - | Partial + | DeepPartial | ((container: SerializedStreamedSpanContainer) => void); type Expected = diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts new file mode 100644 index 000000000000..5e8aedc0458a --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/scenario.ts @@ -0,0 +1,33 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +Sentry.getCurrentScope().setPropagationContext({ + parentSpanId: '1234567890123456', + traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), +}); + +const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, +); + +Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, +); + +Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts new file mode 100644 index 000000000000..5f408fe9eac7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts @@ -0,0 +1,31 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends manually started streamed parallel root spans in root context', async () => { + expect.assertions(7); + + await createRunner(__dirname, 'scenario.ts') + .expect({ span: { items: [{ name: 'test_span_1' }] } }) + .expect({ + span: spanContainer => { + expect(spanContainer).toBeDefined(); + const traceId = spanContainer.items[0]!.trace_id; + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + + // It ignores propagation context of the root context + expect(traceId).not.toBe('12345678901234567890123456789012'); + expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); + + // Different trace ID than the first span + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts new file mode 100644 index 000000000000..d688d3c3adf7 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/scenario.ts @@ -0,0 +1,29 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +Sentry.withScope(() => { + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); + +Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts new file mode 100644 index 000000000000..a9eb9805b3d2 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts @@ -0,0 +1,29 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends manually started streamed parallel root spans outside of root context', async () => { + expect.assertions(6); + + await createRunner(__dirname, 'scenario.ts') + .expect({ span: { items: [{ name: 'test_span_1' }] } }) + .expect({ + span: spanContainer => { + expect(spanContainer).toBeDefined(); + const traceId = spanContainer.items[0]!.trace_id; + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); + + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts new file mode 100644 index 000000000000..64f194549572 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/scenario.ts @@ -0,0 +1,35 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +Sentry.withScope(scope => { + scope.setPropagationContext({ + parentSpanId: '1234567890123456', + traceId: '12345678901234567890123456789012', + sampleRand: Math.random(), + }); + + const spanIdTraceId = Sentry.startSpan( + { + name: 'test_span_1', + }, + span1 => span1.spanContext().traceId, + ); + + Sentry.startSpan( + { + name: 'test_span_2', + attributes: { spanIdTraceId }, + }, + () => undefined, + ); +}); + +Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts new file mode 100644 index 000000000000..1346cb9e20fd --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts @@ -0,0 +1,29 @@ +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('sends manually started streamed parallel root spans outside of root context with parentSpanId', async () => { + expect.assertions(6); + + await createRunner(__dirname, 'scenario.ts') + .expect({ span: { items: [{ name: 'test_span_1' }] } }) + .expect({ + span: spanContainer => { + expect(spanContainer).toBeDefined(); + const traceId = spanContainer.items[0]!.trace_id; + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); + + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; + expect(trace1Id).toBeDefined(); + + // Different trace ID as the first span + expect(trace1Id).not.toBe(traceId); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index bd81cecdf7f9..859959c8a8e9 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -130,6 +130,12 @@ async function runDockerCompose(options: DockerOptions): Promise { }); } +type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + type ExpectedEvent = Partial | ((event: Event) => void); type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); type ExpectedSession = Partial | ((event: SerializedSession) => void); @@ -139,7 +145,7 @@ type ExpectedClientReport = Partial | ((event: ClientReport) => vo type ExpectedLogContainer = Partial | ((event: SerializedLogContainer) => void); type ExpectedMetricContainer = Partial | ((event: SerializedMetricContainer) => void); type ExpectedSpanContainer = - | Partial + | DeepPartial | ((container: SerializedStreamedSpanContainer) => void); type Expected = diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index d22905670efc..248f0d89dfba 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -124,12 +124,11 @@ export function getStreamedSpanLinks( links?: SpanLink[], ): SpanLinkJSON>>[] | undefined { if (links?.length) { - return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ + return links.map(({ context: { spanId, traceId, traceFlags }, attributes }) => ({ span_id: spanId, trace_id: traceId, sampled: traceFlags === TRACE_FLAG_SAMPLED, attributes, - ...restContext, })); } else { return undefined; From 0f8ea1181c34c07ba45d77e4284000a49a775585 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Mar 2026 16:36:29 +0100 Subject: [PATCH 3/8] add span name update tests --- .../updateName-method-streamed/scenario.ts | 22 ++++++++++++++++ .../updateName-method-streamed/test.ts | 26 +++++++++++++++++++ .../scenario.ts | 22 ++++++++++++++++ .../updateSpanName-function-streamed/test.ts | 26 +++++++++++++++++++ .../updateName-method-streamed/scenario.ts | 19 ++++++++++++++ .../updateName-method-streamed/test.ts | 26 +++++++++++++++++++ .../scenario.ts | 19 ++++++++++++++ .../updateSpanName-function-streamed/test.ts | 26 +++++++++++++++++++ 8 files changed, 186 insertions(+) create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts create mode 100644 dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts new file mode 100644 index 000000000000..8512fb954b8e --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + span.updateName('new name'); + }, +); + +void Sentry.flush(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts new file mode 100644 index 000000000000..09cc140278ed --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts @@ -0,0 +1,26 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core'; +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name when calling `span.updateName` (streamed)', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: { + items: [ + { + name: 'new name', + is_segment: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'url' }, + }, + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts new file mode 100644 index 000000000000..34892b20d692 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts @@ -0,0 +1,22 @@ +import * as Sentry from '@sentry/node-core'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; +import { setupOtel } from '../../../../utils/setupOtel'; + +const client = Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +setupOtel(client); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + Sentry.updateSpanName(span, 'new name'); + }, +); + +void Sentry.flush(); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts new file mode 100644 index 000000000000..8ff4b71ed5e6 --- /dev/null +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts @@ -0,0 +1,26 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node-core'; +import { afterAll, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name and source when calling `updateSpanName` (streamed)', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: { + items: [ + { + name: 'new name', + is_segment: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' }, + }, + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts new file mode 100644 index 000000000000..2cbbaef888ae --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + span.updateName('new name'); + }, +); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts new file mode 100644 index 000000000000..24970ff0b311 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts @@ -0,0 +1,26 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name when calling `span.updateName` (streamed)', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: { + items: [ + { + name: 'new name', + is_segment: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'url' }, + }, + }, + ], + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts new file mode 100644 index 000000000000..93d653d107aa --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/scenario.ts @@ -0,0 +1,19 @@ +import * as Sentry from '@sentry/node'; +import { loggingTransport } from '@sentry-internal/node-integration-tests'; + +Sentry.init({ + dsn: 'https://public@dsn.ingest.sentry.io/1337', + release: '1.0', + tracesSampleRate: 1.0, + traceLifecycle: 'stream', + transport: loggingTransport, +}); + +Sentry.startSpan( + { name: 'test_span', attributes: { [Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url' } }, + (span: Sentry.Span) => { + Sentry.updateSpanName(span, 'new name'); + }, +); + +void Sentry.flush(); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts new file mode 100644 index 000000000000..711038e53285 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts @@ -0,0 +1,26 @@ +import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; +import { afterAll, expect, test } from 'vitest'; +import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; + +afterAll(() => { + cleanupChildProcesses(); +}); + +test('updates the span name and source when calling `updateSpanName` (streamed)', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: { + items: [ + { + name: 'new name', + is_segment: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: { type: 'string', value: 'custom' }, + }, + }, + ], + }, + }) + .start() + .completed(); +}); From 68d98d949e83b10332abeebf85df01e15f96cfec Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 13 Mar 2026 16:40:48 +0100 Subject: [PATCH 4/8] lint --- .../public-api/startSpan/updateName-method-streamed/test.ts | 2 +- .../startSpan/updateSpanName-function-streamed/test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts index 24970ff0b311..f9d15cf60e30 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateName-method-streamed/test.ts @@ -1,5 +1,5 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; -import { afterAll, expect, test } from 'vitest'; +import { afterAll, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts index 711038e53285..ace2f0ca0d76 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/updateSpanName-function-streamed/test.ts @@ -1,5 +1,5 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/node'; -import { afterAll, expect, test } from 'vitest'; +import { afterAll, test } from 'vitest'; import { cleanupChildProcesses, createRunner } from '../../../../utils/runner'; afterAll(() => { From 3bb62300dfa90f9c4484832ef491fb810a5c3a6b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 17 Mar 2026 13:48:58 +0100 Subject: [PATCH 5/8] fix build error --- .../node-core-integration-tests/utils/assertions.ts | 8 +++++++- dev-packages/node-core-integration-tests/utils/runner.ts | 7 +------ dev-packages/node-integration-tests/utils/assertions.ts | 8 +++++++- dev-packages/node-integration-tests/utils/runner.ts | 7 +------ 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/dev-packages/node-core-integration-tests/utils/assertions.ts b/dev-packages/node-core-integration-tests/utils/assertions.ts index 0816e894118b..4f08141d9f93 100644 --- a/dev-packages/node-core-integration-tests/utils/assertions.ts +++ b/dev-packages/node-core-integration-tests/utils/assertions.ts @@ -13,6 +13,12 @@ import type { import { SDK_VERSION } from '@sentry/core'; import { expect } from 'vitest'; +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + /** * Asserts against a Sentry Event ignoring non-deterministic properties * @@ -89,7 +95,7 @@ export function assertSentryMetricContainer( export function assertSentrySpanContainer( actual: SerializedStreamedSpanContainer, - expected: Partial, + expected: DeepPartial, ): void { expect(actual).toMatchObject({ items: expect.any(Array), diff --git a/dev-packages/node-core-integration-tests/utils/runner.ts b/dev-packages/node-core-integration-tests/utils/runner.ts index f40ea4fb1c65..d27c65fc81be 100644 --- a/dev-packages/node-core-integration-tests/utils/runner.ts +++ b/dev-packages/node-core-integration-tests/utils/runner.ts @@ -18,6 +18,7 @@ import { execSync, spawn, spawnSync } from 'child_process'; import { existsSync, readFileSync, unlinkSync, writeFileSync } from 'fs'; import { join } from 'path'; import { afterAll, beforeAll, describe, test } from 'vitest'; +import type { DeepPartial } from './assertions'; import { assertEnvelopeHeader, assertSentryCheckIn, @@ -120,12 +121,6 @@ async function runDockerCompose(options: DockerOptions): Promise { }); } -type DeepPartial = T extends object - ? { - [P in keyof T]?: DeepPartial; - } - : T; - type ExpectedEvent = Partial | ((event: Event) => void); type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); type ExpectedSession = Partial | ((event: SerializedSession) => void); diff --git a/dev-packages/node-integration-tests/utils/assertions.ts b/dev-packages/node-integration-tests/utils/assertions.ts index 8b78ddda0556..b6c45ebc043c 100644 --- a/dev-packages/node-integration-tests/utils/assertions.ts +++ b/dev-packages/node-integration-tests/utils/assertions.ts @@ -13,6 +13,12 @@ import type { import { SDK_VERSION } from '@sentry/core'; import { expect } from 'vitest'; +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; + /** * Asserts against a Sentry Event ignoring non-deterministic properties * @@ -89,7 +95,7 @@ export function assertSentryMetricContainer( export function assertSentrySpanContainer( actual: SerializedStreamedSpanContainer, - expected: Partial, + expected: DeepPartial, ): void { expect(actual).toMatchObject({ items: expect.any(Array), diff --git a/dev-packages/node-integration-tests/utils/runner.ts b/dev-packages/node-integration-tests/utils/runner.ts index 859959c8a8e9..7690fa40ee8b 100644 --- a/dev-packages/node-integration-tests/utils/runner.ts +++ b/dev-packages/node-integration-tests/utils/runner.ts @@ -21,6 +21,7 @@ import { cp, mkdir, readFile, rm, writeFile } from 'fs/promises'; import { basename, join } from 'path'; import { inspect, promisify } from 'util'; import { afterAll, beforeAll, describe, test } from 'vitest'; +import type { DeepPartial } from './assertions'; import { assertEnvelopeHeader, assertSentryCheckIn, @@ -130,12 +131,6 @@ async function runDockerCompose(options: DockerOptions): Promise { }); } -type DeepPartial = T extends object - ? { - [P in keyof T]?: DeepPartial; - } - : T; - type ExpectedEvent = Partial | ((event: Event) => void); type ExpectedTransaction = Partial | ((event: TransactionEvent) => void); type ExpectedSession = Partial | ((event: SerializedSession) => void); From 40b575fefb99b7fee5f41559fe675ca3e83d1d45 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 17 Mar 2026 14:48:55 +0100 Subject: [PATCH 6/8] fix test (sentry.release picked up from CI) --- .../startSpan/basic-usage-streamed/test.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index c7682cc8792c..cfa15f63bbeb 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -48,7 +48,7 @@ test('sends a streamed span envelope with correct spans for a manually started s const childSpan = spans.find(s => s.name === 'test-child-span'); expect(childSpan).toBeDefined(); expect(childSpan).toEqual({ - attributes: { + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test-child', @@ -57,7 +57,7 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }, + }), name: 'test-child-span', is_segment: false, parent_span_id: segmentSpanId, @@ -71,12 +71,12 @@ test('sends a streamed span envelope with correct spans for a manually started s const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); expect(inactiveSpan).toBeDefined(); expect(inactiveSpan).toEqual({ - attributes: { + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }, + }), links: [ { attributes: { @@ -103,12 +103,12 @@ test('sends a streamed span envelope with correct spans for a manually started s const manualSpan = spans.find(s => s.name === 'test-manual-span'); expect(manualSpan).toBeDefined(); expect(manualSpan).toEqual({ - attributes: { + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }, + }), name: 'test-manual-span', is_segment: false, parent_span_id: segmentSpanId, @@ -120,14 +120,14 @@ test('sends a streamed span envelope with correct spans for a manually started s }); expect(segmentSpan).toEqual({ - attributes: { + attributes: expect.objectContaining({ [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }, + }), name: 'test-span', is_segment: true, trace_id: traceId, From 3bd461381e06cef986eac23e4a92c53e19734b6e Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 17 Mar 2026 14:49:56 +0100 Subject: [PATCH 7/8] size-limit --- .size-limit.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.size-limit.js b/.size-limit.js index 3e22ffe29961..88fcb8f276c1 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -317,7 +317,7 @@ module.exports = [ import: createImport('init'), ignore: [...builtinModules, ...nodePrefixedBuiltinModules], gzip: true, - limit: '57 KB', + limit: '59 KB', }, // Node SDK (ESM) { From 8b6dcd161170ceea5fccc7807ec9c14f42a3b12b Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 17 Mar 2026 15:50:35 +0100 Subject: [PATCH 8/8] harden basic usage test --- .../basic-usage-streamed/scenario.ts | 1 + .../startSpan/basic-usage-streamed/test.ts | 21 ++- .../parallel-root-spans-streamed/test.ts | 5 +- .../parallel-spans-in-scope-streamed/test.ts | 4 +- .../test.ts | 4 +- .../scenario.ts | 8 +- .../startSpan/basic-usage-streamed/test.ts | 148 ++++++++++++++++++ .../parallel-root-spans-streamed/test.ts | 5 +- .../parallel-spans-in-scope-streamed/test.ts | 4 +- .../test.ts | 4 +- .../public-api/startSpan/streamed/test.ts | 92 ----------- 11 files changed, 182 insertions(+), 114 deletions(-) rename dev-packages/node-integration-tests/suites/public-api/startSpan/{streamed => basic-usage-streamed}/scenario.ts (66%) create mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts delete mode 100644 dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts index 46d9f4005e6b..cf8c1be967f4 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts @@ -8,6 +8,7 @@ const client = Sentry.init({ traceLifecycle: 'stream', integrations: [Sentry.spanStreamingIntegration()], transport: loggingTransport, + release: '1.0.0', }); setupOtel(client); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts index cfa15f63bbeb..3184aae69d64 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -1,6 +1,7 @@ import { SDK_VERSION, SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, @@ -48,7 +49,7 @@ test('sends a streamed span envelope with correct spans for a manually started s const childSpan = spans.find(s => s.name === 'test-child-span'); expect(childSpan).toBeDefined(); expect(childSpan).toEqual({ - attributes: expect.objectContaining({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test-child', @@ -57,7 +58,8 @@ test('sends a streamed span envelope with correct spans for a manually started s [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }), + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, name: 'test-child-span', is_segment: false, parent_span_id: segmentSpanId, @@ -71,12 +73,13 @@ test('sends a streamed span envelope with correct spans for a manually started s const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); expect(inactiveSpan).toBeDefined(); expect(inactiveSpan).toEqual({ - attributes: expect.objectContaining({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }), + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, links: [ { attributes: { @@ -103,12 +106,13 @@ test('sends a streamed span envelope with correct spans for a manually started s const manualSpan = spans.find(s => s.name === 'test-manual-span'); expect(manualSpan).toBeDefined(); expect(manualSpan).toEqual({ - attributes: expect.objectContaining({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }), + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, name: 'test-manual-span', is_segment: false, parent_span_id: segmentSpanId, @@ -120,14 +124,15 @@ test('sends a streamed span envelope with correct spans for a manually started s }); expect(segmentSpan).toEqual({ - attributes: expect.objectContaining({ + attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node-core' }, [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, - }), + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, name: 'test-span', is_segment: true, trace_id: traceId, diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts index 5f408fe9eac7..f332715bbc42 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts @@ -21,8 +21,9 @@ test('sends manually started streamed parallel root spans in root context', asyn expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); // Different trace ID than the first span - const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; - expect(trace1Id).toBeDefined(); + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value; + expect(trace1Id).toMatch(/^[0-9a-f]{32}$/); + expect(trace1Id).not.toBe(traceId); }, }) diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts index a9eb9805b3d2..02456c9e93b2 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts @@ -17,8 +17,8 @@ test('sends manually started streamed parallel root spans outside of root contex expect(traceId).toMatch(/^[0-9a-f]{32}$/); expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); - const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; - expect(trace1Id).toBeDefined(); + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value; + expect(trace1Id).toMatch(/^[0-9a-f]{32}$/); // Different trace ID as the first span expect(trace1Id).not.toBe(traceId); diff --git a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts index 1346cb9e20fd..325f0f0d5d42 100644 --- a/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts +++ b/dev-packages/node-core-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts @@ -17,8 +17,8 @@ test('sends manually started streamed parallel root spans outside of root contex expect(traceId).toMatch(/^[0-9a-f]{32}$/); expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); - const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; - expect(trace1Id).toBeDefined(); + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value; + expect(trace1Id).toMatch(/^[0-9a-f]{32}$/); // Different trace ID as the first span expect(trace1Id).not.toBe(traceId); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts similarity index 66% rename from dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts rename to dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts index 49cb7a633ef5..ce3f914d51b8 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/scenario.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/scenario.ts @@ -7,14 +7,18 @@ Sentry.init({ traceLifecycle: 'stream', integrations: [Sentry.spanStreamingIntegration()], transport: loggingTransport, + release: '1.0.0', }); -Sentry.startSpan({ name: 'test-span', op: 'test' }, () => { +Sentry.startSpan({ name: 'test-span', op: 'test' }, segmentSpan => { Sentry.startSpan({ name: 'test-child-span', op: 'test-child' }, () => { // noop }); - const inactiveSpan = Sentry.startInactiveSpan({ name: 'test-inactive-span' }); + const inactiveSpan = Sentry.startInactiveSpan({ + name: 'test-inactive-span', + links: [{ context: segmentSpan.spanContext(), attributes: { 'sentry.link.type': 'some_relation' } }], + }); inactiveSpan.end(); Sentry.startSpanManual({ name: 'test-manual-span' }, span => { diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts new file mode 100644 index 000000000000..b31ca320df53 --- /dev/null +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/basic-usage-streamed/test.ts @@ -0,0 +1,148 @@ +import { + SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_RELEASE, + SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME, + SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION, + SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID, + SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME, +} from '@sentry/core'; +import { expect, test } from 'vitest'; +import { createRunner } from '../../../../utils/runner'; + +test('sends a streamed span envelope with correct envelope header', async () => { + await createRunner(__dirname, 'scenario.ts') + .expectHeader({ + span: { + sent_at: expect.any(String), + sdk: { + name: 'sentry.javascript.node', + version: SDK_VERSION, + }, + trace: expect.objectContaining({ + public_key: 'public', + sample_rate: '1', + sampled: 'true', + trace_id: expect.stringMatching(/^[\da-f]{32}$/), + transaction: 'test-span', + }), + }, + }) + .start() + .completed(); +}); + +test('sends a streamed span envelope with correct spans for a manually started span with children', async () => { + await createRunner(__dirname, 'scenario.ts') + .expect({ + span: container => { + const spans = container.items; + expect(spans.length).toBe(4); + + const segmentSpan = spans.find(s => !!s.is_segment); + expect(segmentSpan).toBeDefined(); + + const segmentSpanId = segmentSpan!.span_id; + const traceId = segmentSpan!.trace_id; + + const childSpan = spans.find(s => s.name === 'test-child-span'); + expect(childSpan).toBeDefined(); + expect(childSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { + type: 'string', + value: 'test-child', + }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, + name: 'test-child-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); + expect(inactiveSpan).toBeDefined(); + expect(inactiveSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, + links: [ + { + attributes: { + 'sentry.link.type': { + type: 'string', + value: 'some_relation', + }, + }, + sampled: true, + span_id: segmentSpanId, + trace_id: traceId, + }, + ], + name: 'test-inactive-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + const manualSpan = spans.find(s => s.name === 'test-manual-span'); + expect(manualSpan).toBeDefined(); + expect(manualSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, + name: 'test-manual-span', + is_segment: false, + parent_span_id: segmentSpanId, + trace_id: traceId, + span_id: expect.stringMatching(/^[\da-f]{16}$/), + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + + expect(segmentSpan).toEqual({ + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: { type: 'integer', value: 1 }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_NAME]: { type: 'string', value: 'sentry.javascript.node' }, + [SEMANTIC_ATTRIBUTE_SENTRY_SDK_VERSION]: { type: 'string', value: SDK_VERSION }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_ID]: { type: 'string', value: segmentSpanId }, + [SEMANTIC_ATTRIBUTE_SENTRY_SEGMENT_NAME]: { type: 'string', value: 'test-span' }, + [SEMANTIC_ATTRIBUTE_SENTRY_RELEASE]: { type: 'string', value: '1.0.0' }, + }, + name: 'test-span', + is_segment: true, + trace_id: traceId, + span_id: segmentSpanId, + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + }); + }, + }) + .start() + .completed(); +}); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts index 5f408fe9eac7..f332715bbc42 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-root-spans-streamed/test.ts @@ -21,8 +21,9 @@ test('sends manually started streamed parallel root spans in root context', asyn expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); // Different trace ID than the first span - const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; - expect(trace1Id).toBeDefined(); + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value; + expect(trace1Id).toMatch(/^[0-9a-f]{32}$/); + expect(trace1Id).not.toBe(traceId); }, }) diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts index a9eb9805b3d2..02456c9e93b2 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-streamed/test.ts @@ -17,8 +17,8 @@ test('sends manually started streamed parallel root spans outside of root contex expect(traceId).toMatch(/^[0-9a-f]{32}$/); expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); - const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; - expect(trace1Id).toBeDefined(); + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value; + expect(trace1Id).toMatch(/^[0-9a-f]{32}$/); // Different trace ID as the first span expect(trace1Id).not.toBe(traceId); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts index 1346cb9e20fd..325f0f0d5d42 100644 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts +++ b/dev-packages/node-integration-tests/suites/public-api/startSpan/parallel-spans-in-scope-with-parentSpanId-streamed/test.ts @@ -17,8 +17,8 @@ test('sends manually started streamed parallel root spans outside of root contex expect(traceId).toMatch(/^[0-9a-f]{32}$/); expect(spanContainer.items[0]!.parent_span_id).toBeUndefined(); - const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId; - expect(trace1Id).toBeDefined(); + const trace1Id = spanContainer.items[0]!.attributes?.spanIdTraceId?.value; + expect(trace1Id).toMatch(/^[0-9a-f]{32}$/); // Different trace ID as the first span expect(trace1Id).not.toBe(traceId); diff --git a/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts b/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts deleted file mode 100644 index 87d7021929a1..000000000000 --- a/dev-packages/node-integration-tests/suites/public-api/startSpan/streamed/test.ts +++ /dev/null @@ -1,92 +0,0 @@ -import { SDK_VERSION } from '@sentry/core'; -import { expect, test } from 'vitest'; -import { createRunner } from '../../../../utils/runner'; - -test('sends a streamed span envelope with correct envelope header', async () => { - await createRunner(__dirname, 'scenario.ts') - .expectHeader({ - span: { - sent_at: expect.any(String), - sdk: { - name: 'sentry.javascript.node', - version: SDK_VERSION, - }, - trace: expect.objectContaining({ - public_key: 'public', - sample_rate: '1', - sampled: 'true', - trace_id: expect.stringMatching(/^[\da-f]{32}$/), - transaction: 'test-span', - }), - }, - }) - .start() - .completed(); -}); - -test('sends a streamed span envelope with correct spans for a manually started span with children', async () => { - await createRunner(__dirname, 'scenario.ts') - .expect({ - span: container => { - const spans = container.items; - expect(spans.length).toBe(4); - - const segmentSpan = spans.find(s => !!s.is_segment); - expect(segmentSpan).toBeDefined(); - - const segmentSpanId = segmentSpan!.span_id; - const traceId = segmentSpan!.trace_id; - - const childSpan = spans.find(s => s.name === 'test-child-span'); - expect(childSpan).toBeDefined(); - expect(childSpan).toMatchObject({ - name: 'test-child-span', - is_segment: false, - parent_span_id: segmentSpanId, - trace_id: traceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - status: 'ok', - }); - - const inactiveSpan = spans.find(s => s.name === 'test-inactive-span'); - expect(inactiveSpan).toBeDefined(); - expect(inactiveSpan).toMatchObject({ - name: 'test-inactive-span', - is_segment: false, - parent_span_id: segmentSpanId, - trace_id: traceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - status: 'ok', - }); - - const manualSpan = spans.find(s => s.name === 'test-manual-span'); - expect(manualSpan).toBeDefined(); - expect(manualSpan).toMatchObject({ - name: 'test-manual-span', - is_segment: false, - parent_span_id: segmentSpanId, - trace_id: traceId, - span_id: expect.stringMatching(/^[\da-f]{16}$/), - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - status: 'ok', - }); - - expect(segmentSpan).toMatchObject({ - name: 'test-span', - is_segment: true, - trace_id: traceId, - span_id: segmentSpanId, - start_timestamp: expect.any(Number), - end_timestamp: expect.any(Number), - status: 'ok', - }); - }, - }) - .start() - .completed(); -});