From e54e151b2cd50e649ae3295363af367b6d5724a8 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 3 Feb 2026 15:49:01 +0100 Subject: [PATCH 01/10] feat(core): Add span serialization utilities for V2 spans This PR adds utilities for serializing spans to the new V2 format: - `spanToV2JSON`: Converts a span to SerializedSpan (V2 format) - `getV2SpanLinks`: Converts span links with serialized attributes - `getV2StatusMessage`: Converts status to 'ok' | 'error' - `INTERNAL_getSegmentSpan`: Renamed from getRootSpan (with alias kept) These utilities are needed for the span streaming feature and will be used by subsequent PRs to serialize spans before sending. Co-authored-by: Cursor --- packages/core/src/index.ts | 4 ++ packages/core/src/utils/spanUtils.ts | 90 +++++++++++++++++++++++++++- 2 files changed, 92 insertions(+), 2 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index b05934465dfb..18e77fe9e98c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -76,13 +76,17 @@ export { addAutoIpAddressToSession } from './utils/ipAddress'; export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, + getV2SpanLinks, spanToTraceHeader, spanToJSON, + spanToV2JSON, spanIsSampled, spanToTraceContext, getSpanDescendants, getStatusMessage, + getV2StatusMessage, getRootSpan, + INTERNAL_getSegmentSpan, getActiveSpan, addChildSpanToSpan, spanTimeInputToSeconds, diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index d7c261ecd73c..4b051a5d8d78 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,4 +1,6 @@ import { getAsyncContextStrategy } from '../asyncContext'; +import type { Attributes } from '../attributes'; +import { serializeAttributes } from '../attributes'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; import { @@ -12,7 +14,7 @@ import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; import { getCapturedScopesOnSpan } from '../tracing/utils'; import type { TraceContext } from '../types-hoist/context'; import type { SpanLink, SpanLinkJSON } from '../types-hoist/link'; -import type { Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span'; +import type { SerializedSpan, Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; import { addNonEnumerableProperty } from '../utils/object'; import { generateSpanId } from '../utils/propagationContext'; @@ -105,6 +107,25 @@ export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[] } } +/** + * Converts the span links array to a flattened version with serialized attributes for V2 spans. + * + * If the links array is empty, it returns `undefined` so the empty value can be dropped before it's sent. + */ +export function getV2SpanLinks(links?: SpanLink[]): SpanLinkJSON[] | undefined { + if (links?.length) { + return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ + span_id: spanId, + trace_id: traceId, + sampled: traceFlags === TRACE_FLAG_SAMPLED, + ...(attributes && { attributes: serializeAttributes(attributes) }), + ...restContext, + })); + } else { + return undefined; + } +} + /** * Convert a span time input into a timestamp in seconds. */ @@ -187,6 +208,59 @@ export function spanToJSON(span: Span): SpanJSON { }; } +/** + * Convert a span to a SerializedSpan representation (V2 span format). + */ +export function spanToV2JSON(span: Span): SerializedSpan { + // Check if the span has a getSpanV2JSON method (added in SentrySpan in later PRs) + if (typeof (span as SentrySpan & { getSpanV2JSON?: () => SerializedSpan }).getSpanV2JSON === 'function') { + return (span as SentrySpan & { getSpanV2JSON: () => SerializedSpan }).getSpanV2JSON(); + } + + const { spanId: span_id, traceId: trace_id } = span.spanContext(); + + // Handle a span from @opentelemetry/sdk-base-trace's `Span` class + if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { + const { attributes, startTime, name, endTime, status, links } = span; + + // In preparation for the next major of OpenTelemetry, we want to support + // looking up the parent span id according to the new API + // In OTel v1, the parent span id is accessed as `parentSpanId` + // In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` + const parentSpanId = + 'parentSpanId' in span + ? span.parentSpanId + : 'parentSpanContext' in span + ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId + : undefined; + + return { + name, + span_id, + trace_id, + parent_span_id: parentSpanId, + start_timestamp: spanTimeInputToSeconds(startTime), + end_timestamp: spanTimeInputToSeconds(endTime), + is_segment: span === INTERNAL_getSegmentSpan(span), + status: getV2StatusMessage(status), + attributes: serializeAttributes(attributes), + links: getV2SpanLinks(links), + }; + } + + // Finally, as a fallback, at least we have `spanContext()`.... + // This should not actually happen in reality, but we need to handle it for type safety. + return { + span_id, + trace_id, + start_timestamp: 0, + name: '', + end_timestamp: 0, + status: 'ok', + is_segment: span === INTERNAL_getSegmentSpan(span), + }; +} + function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan { const castSpan = span as Partial; return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status; @@ -237,6 +311,13 @@ export function getStatusMessage(status: SpanStatus | undefined): string | undef return status.message || 'internal_error'; } +/** + * Convert the various statuses to the ones expected by Sentry for V2 spans ('ok' is default). + */ +export function getV2StatusMessage(status: SpanStatus | undefined): 'ok' | 'error' { + return !status || status.code === SPAN_STATUS_OK || status.code === SPAN_STATUS_UNSET ? 'ok' : 'error'; +} + const CHILD_SPANS_FIELD = '_sentryChildSpans'; const ROOT_SPAN_FIELD = '_sentryRootSpan'; @@ -298,7 +379,12 @@ export function getSpanDescendants(span: SpanWithPotentialChildren): Span[] { /** * Returns the root span of a given span. */ -export function getRootSpan(span: SpanWithPotentialChildren): Span { +export const getRootSpan = INTERNAL_getSegmentSpan; + +/** + * Returns the segment span of a given span. + */ +export function INTERNAL_getSegmentSpan(span: SpanWithPotentialChildren): Span { return span[ROOT_SPAN_FIELD] || span; } From 2774fed56aec761a3805915606def1a7ad4a3899 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 3 Feb 2026 16:38:01 +0100 Subject: [PATCH 02/10] rename, add tests, add 2 stage conversion --- packages/core/src/index.ts | 4 +- packages/core/src/tracing/sentrySpan.ts | 28 +++ packages/core/src/utils/spanUtils.ts | 52 +++- .../core/test/lib/utils/spanUtils.test.ts | 235 +++++++++++++++++- 4 files changed, 302 insertions(+), 17 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 18e77fe9e98c..ed369cef445c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -76,15 +76,13 @@ export { addAutoIpAddressToSession } from './utils/ipAddress'; export { addAutoIpAddressToUser } from './utils/ipAddress'; export { convertSpanLinksForEnvelope, - getV2SpanLinks, spanToTraceHeader, spanToJSON, - spanToV2JSON, + spanToStreamedSpanJSON, spanIsSampled, spanToTraceContext, getSpanDescendants, getStatusMessage, - getV2StatusMessage, getRootSpan, INTERNAL_getSegmentSpan, getActiveSpan, diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index 9bd98b9741c6..a35ef9d0e4f9 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-lines */ import { getClient, getCurrentScope } from '../currentScopes'; import { DEBUG_BUILD } from '../debug-build'; import { createSpanEnvelope } from '../envelope'; @@ -21,6 +22,7 @@ import type { SpanJSON, SpanOrigin, SpanTimeInput, + StreamedSpanJSON, } from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; import type { TimedEvent } from '../types-hoist/timedEvent'; @@ -29,8 +31,10 @@ import { generateSpanId, generateTraceId } from '../utils/propagationContext'; import { convertSpanLinksForEnvelope, getRootSpan, + getSimpleStatusMessage, getSpanDescendants, getStatusMessage, + getStreamedSpanLinks, spanTimeInputToSeconds, spanToJSON, spanToTransactionTraceContext, @@ -241,6 +245,30 @@ export class SentrySpan implements Span { }; } + /** + * Get {@link StreamedSpanJSON} representation of this span. + * + * @hidden + * @internal This method is purely for internal purposes and should not be used outside + * of SDK code. If you need to get a JSON representation of a span, + * use `spanToV2JSON(span)` instead. + */ + public getStreamedSpanJSON(): StreamedSpanJSON { + return { + name: this._name ?? '', + span_id: this._spanId, + trace_id: this._traceId, + parent_span_id: this._parentSpanId, + start_timestamp: this._startTime, + // just in case _endTime is not set, we use the start time (i.e. duration 0) + end_timestamp: this._endTime ?? this._startTime, + is_segment: this._isStandaloneSpan || this === getRootSpan(this), + status: getSimpleStatusMessage(this._status), + attributes: this._attributes, + links: getStreamedSpanLinks(this._links), + }; + } + /** @inheritdoc */ public isRecording(): boolean { return !this._endTime && !!this._sampled; diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 4b051a5d8d78..85e63287783e 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,5 +1,5 @@ import { getAsyncContextStrategy } from '../asyncContext'; -import type { Attributes } from '../attributes'; +import type { Attributes, RawAttributes } from '../attributes'; import { serializeAttributes } from '../attributes'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; @@ -14,7 +14,15 @@ import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing/spanstatus'; import { getCapturedScopesOnSpan } from '../tracing/utils'; import type { TraceContext } from '../types-hoist/context'; import type { SpanLink, SpanLinkJSON } from '../types-hoist/link'; -import type { SerializedSpan, Span, SpanAttributes, SpanJSON, SpanOrigin, SpanTimeInput } from '../types-hoist/span'; +import type { + SerializedSpan, + Span, + SpanAttributes, + SpanJSON, + SpanOrigin, + SpanTimeInput, + StreamedSpanJSON, +} from '../types-hoist/span'; import type { SpanStatus } from '../types-hoist/spanStatus'; import { addNonEnumerableProperty } from '../utils/object'; import { generateSpanId } from '../utils/propagationContext'; @@ -112,13 +120,15 @@ export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[] * * If the links array is empty, it returns `undefined` so the empty value can be dropped before it's sent. */ -export function getV2SpanLinks(links?: SpanLink[]): SpanLinkJSON[] | undefined { +export function getStreamedSpanLinks( + links?: SpanLink[], +): SpanLinkJSON>>[] | undefined { if (links?.length) { return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({ span_id: spanId, trace_id: traceId, sampled: traceFlags === TRACE_FLAG_SAMPLED, - ...(attributes && { attributes: serializeAttributes(attributes) }), + attributes, ...restContext, })); } else { @@ -209,12 +219,12 @@ export function spanToJSON(span: Span): SpanJSON { } /** - * Convert a span to a SerializedSpan representation (V2 span format). + * Convert a span to the intermediate {@link StreamedSpanJSON} representation. */ -export function spanToV2JSON(span: Span): SerializedSpan { +export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { // Check if the span has a getSpanV2JSON method (added in SentrySpan in later PRs) - if (typeof (span as SentrySpan & { getSpanV2JSON?: () => SerializedSpan }).getSpanV2JSON === 'function') { - return (span as SentrySpan & { getSpanV2JSON: () => SerializedSpan }).getSpanV2JSON(); + if (typeof (span as SentrySpan & { getSpanV2JSON?: () => SerializedSpan }).getStreamedSpanJSON === 'function') { + return (span as SentrySpan & { getSpanV2JSON: () => SerializedSpan }).getStreamedSpanJSON(); } const { spanId: span_id, traceId: trace_id } = span.spanContext(); @@ -242,9 +252,9 @@ export function spanToV2JSON(span: Span): SerializedSpan { start_timestamp: spanTimeInputToSeconds(startTime), end_timestamp: spanTimeInputToSeconds(endTime), is_segment: span === INTERNAL_getSegmentSpan(span), - status: getV2StatusMessage(status), - attributes: serializeAttributes(attributes), - links: getV2SpanLinks(links), + status: getSimpleStatusMessage(status), + attributes, + links: getStreamedSpanLinks(links), }; } @@ -261,6 +271,22 @@ export function spanToV2JSON(span: Span): SerializedSpan { }; } +/** + * Converts a {@link StreamedSpanJSON} to a {@link SerializedSpan}. + * This is the final serialized span format that is sent to Sentry. + * The returned serilaized spans must not be consumed by users or SDK integrations. + */ +export function spanJsonToSerializedSpan(spanJson: StreamedSpanJSON): SerializedSpan { + return { + ...spanJson, + attributes: serializeAttributes(spanJson.attributes), + links: spanJson.links?.map(link => ({ + ...link, + attributes: serializeAttributes(link.attributes), + })), + }; +} + function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan { const castSpan = span as Partial; return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status; @@ -312,9 +338,9 @@ export function getStatusMessage(status: SpanStatus | undefined): string | undef } /** - * Convert the various statuses to the ones expected by Sentry for V2 spans ('ok' is default). + * Convert the various statuses to the simple onces expected by Sentry for steamed spans ('ok' is default). */ -export function getV2StatusMessage(status: SpanStatus | undefined): 'ok' | 'error' { +export function getSimpleStatusMessage(status: SpanStatus | undefined): 'ok' | 'error' { return !status || status.code === SPAN_STATUS_OK || status.code === SPAN_STATUS_UNSET ? 'ok' : 'error'; } diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index bca9a406dd50..f3ce889245b0 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -4,6 +4,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, + SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE, SentrySpan, setCurrentClient, SPAN_STATUS_ERROR, @@ -16,14 +17,16 @@ import { TRACEPARENT_REGEXP, } from '../../../src'; import type { SpanLink } from '../../../src/types-hoist/link'; -import type { Span, SpanAttributes, SpanTimeInput } from '../../../src/types-hoist/span'; +import type { Span, SpanAttributes, SpanTimeInput, StreamedSpanJSON } from '../../../src/types-hoist/span'; import type { SpanStatus } from '../../../src/types-hoist/spanStatus'; import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils'; import { getRootSpan, spanIsSampled, + spanJsonToSerializedSpan, spanTimeInputToSeconds, spanToJSON, + spanToStreamedSpanJSON, spanToTraceContext, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, @@ -41,6 +44,7 @@ function createMockedOtelSpan({ status = { code: SPAN_STATUS_UNSET }, endTime = Date.now(), parentSpanId, + links = undefined, }: { spanId: string; traceId: string; @@ -51,6 +55,7 @@ function createMockedOtelSpan({ status?: SpanStatus; endTime?: SpanTimeInput; parentSpanId?: string; + links?: SpanLink[]; }): Span { return { spanContext: () => { @@ -66,6 +71,7 @@ function createMockedOtelSpan({ status, endTime, parentSpanId, + links, } as OpenTelemetrySdkTraceBaseSpan; } @@ -409,6 +415,233 @@ describe('spanToJSON', () => { }); }); + describe('spanToStreamedSpanJSON', () => { + describe('SentrySpan', () => { + it('converts a minimal span', () => { + const span = new SentrySpan(); + expect(spanToStreamedSpanJSON(span)).toEqual({ + span_id: expect.stringMatching(/^[0-9a-f]{16}$/), + trace_id: expect.stringMatching(/^[0-9a-f]{32}$/), + name: '', + start_timestamp: expect.any(Number), + end_timestamp: expect.any(Number), + status: 'ok', + is_segment: true, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'manual', + }, + }); + }); + + it('converts a full span', () => { + const span = new SentrySpan({ + op: 'test op', + name: 'test name', + parentSpanId: '1234', + spanId: '5678', + traceId: 'abcd', + startTimestamp: 123, + endTimestamp: 456, + attributes: { + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + attr1: 'value1', + attr2: 2, + attr3: true, + }, + links: [ + { + context: { + spanId: 'span1', + traceId: 'trace1', + traceFlags: TRACE_FLAG_SAMPLED, + }, + attributes: { + 'sentry.link.type': 'previous_trace', + }, + }, + ], + }); + span.setStatus({ code: SPAN_STATUS_OK }); + span.setAttribute('attr4', [1, 2, 3]); + + expect(spanToStreamedSpanJSON(span)).toEqual({ + name: 'test name', + parent_span_id: '1234', + span_id: '5678', + trace_id: 'abcd', + start_timestamp: 123, + end_timestamp: 456, + status: 'ok', + is_segment: true, + attributes: { + attr1: 'value1', + attr2: 2, + attr3: true, + attr4: [1, 2, 3], + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + links: [ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace', + }, + }, + ], + }); + }); + }); + describe('OpenTelemetry Span', () => { + it('converts a simple span', () => { + const span = createMockedOtelSpan({ + spanId: 'SPAN-1', + traceId: 'TRACE-1', + name: 'test span', + startTime: 123, + endTime: [0, 0], + attributes: {}, + status: { code: SPAN_STATUS_UNSET }, + }); + + expect(spanToStreamedSpanJSON(span)).toEqual({ + span_id: 'SPAN-1', + trace_id: 'TRACE-1', + parent_span_id: undefined, + start_timestamp: 123, + end_timestamp: 0, + name: 'test span', + is_segment: true, + status: 'ok', + attributes: {}, + }); + }); + + it('converts a full span', () => { + const span = createMockedOtelSpan({ + spanId: 'SPAN-1', + traceId: 'TRACE-1', + parentSpanId: 'PARENT-1', + name: 'test span', + startTime: 123, + endTime: 456, + attributes: { + attr1: 'value1', + attr2: 2, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + links: [ + { + context: { + spanId: 'span1', + traceId: 'trace1', + traceFlags: TRACE_FLAG_SAMPLED, + }, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace', + }, + }, + ], + status: { code: SPAN_STATUS_ERROR, message: 'unknown_error' }, + }); + + expect(spanToStreamedSpanJSON(span)).toEqual({ + span_id: 'SPAN-1', + trace_id: 'TRACE-1', + parent_span_id: 'PARENT-1', + start_timestamp: 123, + end_timestamp: 456, + name: 'test span', + is_segment: true, + status: 'error', + attributes: { + attr1: 'value1', + attr2: 2, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + links: [ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace', + }, + }, + ], + }); + }); + }); + }); + + describe('spanJsonToSerializedSpan', () => { + it('converts a streamed span JSON with links to a serialized span', () => { + const spanJson: StreamedSpanJSON = { + name: 'test name', + parent_span_id: '1234', + span_id: '5678', + trace_id: 'abcd', + start_timestamp: 123, + end_timestamp: 456, + status: 'ok', + is_segment: true, + attributes: { + attr1: 'value1', + attr2: 2, + attr3: true, + attr4: [1, 2, 3], + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'test op', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto', + }, + links: [ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace', + }, + }, + ], + }; + + expect(spanJsonToSerializedSpan(spanJson)).toEqual({ + name: 'test name', + parent_span_id: '1234', + span_id: '5678', + trace_id: 'abcd', + start_timestamp: 123, + end_timestamp: 456, + status: 'ok', + is_segment: true, + attributes: { + attr1: { type: 'string', value: 'value1' }, + attr2: { type: 'integer', value: 2 }, + attr3: { type: 'boolean', value: true }, + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: { type: 'string', value: 'test op' }, + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: { type: 'string', value: 'auto' }, + // notice the absence of `attr4`! + // for now, we don't yet serialize array attributes. This test will fail + // once we allow serializing them. + }, + links: [ + { + span_id: 'span1', + trace_id: 'trace1', + sampled: true, + attributes: { + [SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: { type: 'string', value: 'previous_trace' }, + }, + }, + ], + }); + }); + }); + it('returns minimal object for unknown span implementation', () => { const span = { // This is the minimal interface we require from a span From 5a6d17b6163fd50354a24595d1e74233f4ac4abe Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Tue, 3 Feb 2026 16:42:42 +0100 Subject: [PATCH 03/10] fix lint errro --- packages/core/src/utils/spanUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 85e63287783e..d2c787f085e0 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -1,5 +1,5 @@ import { getAsyncContextStrategy } from '../asyncContext'; -import type { Attributes, RawAttributes } from '../attributes'; +import type { RawAttributes } from '../attributes'; import { serializeAttributes } from '../attributes'; import { getMainCarrier } from '../carrier'; import { getCurrentScope } from '../currentScopes'; From fc18e2210c869270e641809d666c37ea7ebf6801 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 13:11:56 +0100 Subject: [PATCH 04/10] Update packages/core/src/tracing/sentrySpan.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jan Peer Stöcklmair --- packages/core/src/tracing/sentrySpan.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/tracing/sentrySpan.ts b/packages/core/src/tracing/sentrySpan.ts index a35ef9d0e4f9..8bdae7129dba 100644 --- a/packages/core/src/tracing/sentrySpan.ts +++ b/packages/core/src/tracing/sentrySpan.ts @@ -251,7 +251,7 @@ export class SentrySpan implements Span { * @hidden * @internal This method is purely for internal purposes and should not be used outside * of SDK code. If you need to get a JSON representation of a span, - * use `spanToV2JSON(span)` instead. + * use `spanToStreamedSpanJSON(span)` instead. */ public getStreamedSpanJSON(): StreamedSpanJSON { return { From 2c7a20a8b005a83abae9afb944a1e2c6c09dae36 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 13:15:28 +0100 Subject: [PATCH 05/10] remove unnecessary guard and clear up spanv2json confusion --- packages/core/src/utils/spanUtils.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index d2c787f085e0..d734bbcf9aa9 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -222,9 +222,8 @@ export function spanToJSON(span: Span): SpanJSON { * Convert a span to the intermediate {@link StreamedSpanJSON} representation. */ export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { - // Check if the span has a getSpanV2JSON method (added in SentrySpan in later PRs) - if (typeof (span as SentrySpan & { getSpanV2JSON?: () => SerializedSpan }).getStreamedSpanJSON === 'function') { - return (span as SentrySpan & { getSpanV2JSON: () => SerializedSpan }).getStreamedSpanJSON(); + if (spanIsSentrySpan(span)) { + return span.getStreamedSpanJSON(); } const { spanId: span_id, traceId: trace_id } = span.spanContext(); From e6ac9fc4f4dc1d2bb6e280e575f232024ebcd241 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 13:26:17 +0100 Subject: [PATCH 06/10] small optimization --- packages/core/src/utils/spanUtils.ts | 40 +++++++++++----------------- 1 file changed, 16 insertions(+), 24 deletions(-) diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index d734bbcf9aa9..17c8424625c7 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -181,23 +181,12 @@ export function spanToJSON(span: Span): SpanJSON { if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { const { attributes, startTime, name, endTime, status, links } = span; - // In preparation for the next major of OpenTelemetry, we want to support - // looking up the parent span id according to the new API - // In OTel v1, the parent span id is accessed as `parentSpanId` - // In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` - const parentSpanId = - 'parentSpanId' in span - ? span.parentSpanId - : 'parentSpanContext' in span - ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId - : undefined; - return { span_id, trace_id, data: attributes, description: name, - parent_span_id: parentSpanId, + parent_span_id: getOtelParentSpanId(span), start_timestamp: spanTimeInputToSeconds(startTime), // This is [0,0] by default in OTEL, in which case we want to interpret this as no end time timestamp: spanTimeInputToSeconds(endTime) || undefined, @@ -232,22 +221,11 @@ export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) { const { attributes, startTime, name, endTime, status, links } = span; - // In preparation for the next major of OpenTelemetry, we want to support - // looking up the parent span id according to the new API - // In OTel v1, the parent span id is accessed as `parentSpanId` - // In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` - const parentSpanId = - 'parentSpanId' in span - ? span.parentSpanId - : 'parentSpanContext' in span - ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId - : undefined; - return { name, span_id, trace_id, - parent_span_id: parentSpanId, + parent_span_id: getOtelParentSpanId(span), start_timestamp: spanTimeInputToSeconds(startTime), end_timestamp: spanTimeInputToSeconds(endTime), is_segment: span === INTERNAL_getSegmentSpan(span), @@ -270,6 +248,20 @@ export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON { }; } +/** + * In preparation for the next major of OpenTelemetry, we want to support + * looking up the parent span id according to the new API + * In OTel v1, the parent span id is accessed as `parentSpanId` + * In OTel v2, the parent span id is accessed as `spanId` on the `parentSpanContext` + */ +function getOtelParentSpanId(span: OpenTelemetrySdkTraceBaseSpan): string | undefined { + return 'parentSpanId' in span + ? span.parentSpanId + : 'parentSpanContext' in span + ? (span.parentSpanContext as { spanId?: string } | undefined)?.spanId + : undefined; +} + /** * Converts a {@link StreamedSpanJSON} to a {@link SerializedSpan}. * This is the final serialized span format that is sent to Sentry. From a4324e4809dca8403303810caf1ea5c52a8e5742 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 14:33:20 +0100 Subject: [PATCH 07/10] size limit --- .size-limit.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 761905a49ef3..d3070d21c2d4 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -82,7 +82,7 @@ module.exports = [ path: 'packages/browser/build/npm/esm/prod/index.js', import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'), gzip: true, - limit: '85.55 KB', + limit: '86 KB', }, { name: '@sentry/browser (incl. Tracing, Replay, Feedback)', @@ -148,7 +148,7 @@ module.exports = [ import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'), ignore: ['react/jsx-runtime'], gzip: true, - limit: '44.5 KB', + limit: '45 KB', }, // Vue SDK (ESM) { @@ -163,7 +163,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init', 'browserTracingIntegration'), gzip: true, - limit: '44.1 KB', + limit: '45 KB', }, // Svelte SDK (ESM) { @@ -184,7 +184,7 @@ module.exports = [ name: 'CDN Bundle (incl. Tracing)', path: createCDNPath('bundle.tracing.min.js'), gzip: true, - limit: '43 KB', + limit: '44 KB', }, { name: 'CDN Bundle (incl. Logs, Metrics)', From 7ec8aa838bd40d3ec6c8eec95c7b0eafdbf08687 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 14:35:21 +0100 Subject: [PATCH 08/10] s/spanJsonToSerializedSpan/streamedSpanJsonToSerializedSpan --- packages/core/src/utils/spanUtils.ts | 2 +- packages/core/test/lib/utils/spanUtils.test.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/core/src/utils/spanUtils.ts b/packages/core/src/utils/spanUtils.ts index 17c8424625c7..6e4a95b61d7b 100644 --- a/packages/core/src/utils/spanUtils.ts +++ b/packages/core/src/utils/spanUtils.ts @@ -267,7 +267,7 @@ function getOtelParentSpanId(span: OpenTelemetrySdkTraceBaseSpan): string | unde * This is the final serialized span format that is sent to Sentry. * The returned serilaized spans must not be consumed by users or SDK integrations. */ -export function spanJsonToSerializedSpan(spanJson: StreamedSpanJSON): SerializedSpan { +export function streamedSpanJsonToSerializedSpan(spanJson: StreamedSpanJSON): SerializedSpan { return { ...spanJson, attributes: serializeAttributes(spanJson.attributes), diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index f3ce889245b0..08e0e9895c0e 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -23,7 +23,7 @@ import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils import { getRootSpan, spanIsSampled, - spanJsonToSerializedSpan, + streamedSpanJsonToSerializedSpan, spanTimeInputToSeconds, spanToJSON, spanToStreamedSpanJSON, @@ -578,7 +578,7 @@ describe('spanToJSON', () => { }); }); - describe('spanJsonToSerializedSpan', () => { + describe('streamedSpanJsonToSerializedSpan', () => { it('converts a streamed span JSON with links to a serialized span', () => { const spanJson: StreamedSpanJSON = { name: 'test name', @@ -609,7 +609,7 @@ describe('spanToJSON', () => { ], }; - expect(spanJsonToSerializedSpan(spanJson)).toEqual({ + expect(streamedSpanJsonToSerializedSpan(spanJson)).toEqual({ name: 'test name', parent_span_id: '1234', span_id: '5678', From 49b967066b38389ce2def654f54ef48cb930d888 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 15:31:22 +0100 Subject: [PATCH 09/10] vue size limit --- .size-limit.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.size-limit.js b/.size-limit.js index 5c6507e92f5b..8d7ad5e8e3cd 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -156,7 +156,7 @@ module.exports = [ path: 'packages/vue/build/esm/index.js', import: createImport('init'), gzip: true, - limit: '30 KB', + limit: '31 KB', }, { name: '@sentry/vue (incl. Tracing)', @@ -178,7 +178,7 @@ module.exports = [ name: 'CDN Bundle', path: createCDNPath('bundle.min.js'), gzip: true, - limit: '28.5 KB', + limit: '29 KB', }, { name: 'CDN Bundle (incl. Tracing)', From 1eaa99598e82d608d8ad7cd777ff01cf12c2b5e2 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Wed, 4 Feb 2026 15:32:01 +0100 Subject: [PATCH 10/10] lint --- packages/core/test/lib/utils/spanUtils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/test/lib/utils/spanUtils.test.ts b/packages/core/test/lib/utils/spanUtils.test.ts index 08e0e9895c0e..e4a0b31990d7 100644 --- a/packages/core/test/lib/utils/spanUtils.test.ts +++ b/packages/core/test/lib/utils/spanUtils.test.ts @@ -23,11 +23,11 @@ import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils import { getRootSpan, spanIsSampled, - streamedSpanJsonToSerializedSpan, spanTimeInputToSeconds, spanToJSON, spanToStreamedSpanJSON, spanToTraceContext, + streamedSpanJsonToSerializedSpan, TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED, updateSpanName,