diff --git a/CHANGELOG.md b/CHANGELOG.md index 7457cadef6bb..7abc3a5d019b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,6 +49,10 @@ Work in this release was contributed by @adityareddy-dev, @jpdefo, @Jxxunnn, @ph `denoServeIntegration` now records a request session for each incoming request, as `denoHttpIntegration` does for `node:http`. This gives release health and crash-free rates for `Deno.serve` apps. Sessions are sent only when a `release` is set. To turn them off, set `denoServeIntegration({ sessions: false })`. `sessionFlushingDelayMS` sets how long the SDK waits before it sends the session aggregate. +- **fix(core): Preserve span links through `beforeSendSpan`** + + A transaction's `contexts.trace.links` (including the `previous_trace` link) was dropped whenever `beforeSendSpan` was set, even for an identity callback, because the transaction-event/span-JSON round trip did not carry `links`. The conversion now preserves them. + ## 11.8.0 ### Important Changes diff --git a/packages/core/src/utils/transactionEvent.ts b/packages/core/src/utils/transactionEvent.ts index 97bf3580ce8c..4a4fe979ab05 100644 --- a/packages/core/src/utils/transactionEvent.ts +++ b/packages/core/src/utils/transactionEvent.ts @@ -6,7 +6,7 @@ import type { SpanJSON } from '../types/span'; * Converts a transaction event to a span JSON object. */ export function convertTransactionEventToSpanJson(event: TransactionEvent): SpanJSON { - const { trace_id, parent_span_id, span_id, status, origin, data, op } = event.contexts?.trace ?? {}; + const { trace_id, parent_span_id, span_id, status, origin, data, op, links } = event.contexts?.trace ?? {}; return { data: data ?? {}, @@ -19,6 +19,7 @@ export function convertTransactionEventToSpanJson(event: TransactionEvent): Span timestamp: event.timestamp, trace_id: trace_id ?? '', origin, + links, profile_id: data?.[SENTRY_PROFILE_ID] as string | undefined, exclusive_time: data?.[SENTRY_EXCLUSIVE_TIME] as number | undefined, measurements: event.measurements, @@ -43,6 +44,7 @@ export function convertSpanJsonToTransactionEvent(span: SpanJSON): TransactionEv op: span.op, status: span.status, origin: span.origin, + links: span.links, data: { ...span.data, ...(span.profile_id && { [SENTRY_PROFILE_ID]: span.profile_id }), diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts index 9dcfe95fd34a..23078cc4c398 100644 --- a/packages/core/test/lib/client.test.ts +++ b/packages/core/test/lib/client.test.ts @@ -1596,6 +1596,37 @@ describe('Client', () => { }); }); + test('keeps root span links on the sent transaction with `beforeSendSpan`', () => { + const beforeSendSpan = withStaticSpan(vi.fn((span: SpanJSON) => span)); + const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, beforeSendSpan }); + const client = new TestClient(options); + + const links = [ + { + trace_id: '12312012123120121231201212312012', + span_id: '1231201212312012', + sampled: true, + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ]; + + client.captureEvent({ + transaction: '/animals/are/great', + type: 'transaction', + spans: [], + contexts: { + trace: { + span_id: '9e15bf99fbe4bc80', + trace_id: '86f39e84263a4de99c326acab3bfe3bd', + links, + }, + }, + }); + + expect(beforeSendSpan).toHaveBeenCalledTimes(1); + expect(TestClient.instance!.event!.contexts?.trace?.links).toEqual(links); + }); + test('calls `beforeSendTransaction` and uses the modified event', () => { expect.assertions(2); diff --git a/packages/core/test/lib/utils/transactionEvent.test.ts b/packages/core/test/lib/utils/transactionEvent.test.ts index 03f1e231d975..062ec8aa41dc 100644 --- a/packages/core/test/lib/utils/transactionEvent.test.ts +++ b/packages/core/test/lib/utils/transactionEvent.test.ts @@ -48,6 +48,14 @@ describe('convertTransactionEventToSpanJson', () => { [SENTRY_EXCLUSIVE_TIME]: 123.45, other: 'value', }, + links: [ + { + span_id: 'linked-span', + trace_id: 'linked-trace', + sampled: true, + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], }, }, start_timestamp: 1234567800, @@ -74,6 +82,14 @@ describe('convertTransactionEventToSpanJson', () => { origin: 'manual', profile_id: 'profile123', exclusive_time: 123.45, + links: [ + { + span_id: 'linked-span', + trace_id: 'linked-trace', + sampled: true, + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], measurements: { fp: { value: 123, unit: 'millisecond' }, }, @@ -142,6 +158,14 @@ describe('convertSpanJsonToTransactionEvent', () => { origin: 'manual', profile_id: 'profile123', exclusive_time: 123.45, + links: [ + { + span_id: 'linked-span', + trace_id: 'linked-trace', + sampled: true, + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], measurements: { fp: { value: 123, unit: 'millisecond' }, }, @@ -160,6 +184,14 @@ describe('convertSpanJsonToTransactionEvent', () => { op: 'http', status: 'ok', origin: 'manual', + links: [ + { + span_id: 'linked-span', + trace_id: 'linked-trace', + sampled: true, + attributes: { 'sentry.link.type': 'previous_trace' }, + }, + ], data: { other: 'value', [SENTRY_PROFILE_ID]: 'profile123',