Skip to content
6 changes: 3 additions & 3 deletions .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)',
Expand All @@ -178,13 +178,13 @@ module.exports = [
name: 'CDN Bundle',
path: createCDNPath('bundle.min.js'),
gzip: true,
limit: '28.5 KB',
limit: '29 KB',
},
{
name: 'CDN Bundle (incl. Tracing)',
path: createCDNPath('bundle.tracing.min.js'),
gzip: true,
limit: '43.5 KB',
limit: '44 KB',
},
{
name: 'CDN Bundle (incl. Logs, Metrics)',
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,13 @@ export {
convertSpanLinksForEnvelope,
spanToTraceHeader,
spanToJSON,
spanToStreamedSpanJSON,
spanIsSampled,
spanToTraceContext,
getSpanDescendants,
getStatusMessage,
getRootSpan,
INTERNAL_getSegmentSpan,
getActiveSpan,
addChildSpanToSpan,
spanTimeInputToSeconds,
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-lines */
import { getClient, getCurrentScope } from '../currentScopes';
import { DEBUG_BUILD } from '../debug-build';
import { createSpanEnvelope } from '../envelope';
Expand All @@ -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';
Expand All @@ -29,8 +31,10 @@ import { generateSpanId, generateTraceId } from '../utils/propagationContext';
import {
convertSpanLinksForEnvelope,
getRootSpan,
getSimpleStatusMessage,
getSpanDescendants,
getStatusMessage,
getStreamedSpanLinks,
spanTimeInputToSeconds,
spanToJSON,
spanToTransactionTraceContext,
Expand Down Expand Up @@ -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 `spanToStreamedSpanJSON(span)` instead.
*/
public getStreamedSpanJSON(): StreamedSpanJSON {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this even more visible that this is an internal API: Should this maybe prefixed with _INTERNAL_?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Users should never interact with SentrySpan directly but leave it up to start*Span* APIs which kind of Span (as in interface) they get back or work with.

I oriented myself here primarily on the already existing getSpanJSON method. In light of keeping the name short for bundle size, I'd tend to keep it that way, unless you think we should still do it?

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;
Expand Down
131 changes: 117 additions & 14 deletions packages/core/src/utils/spanUtils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { getAsyncContextStrategy } from '../asyncContext';
import type { RawAttributes } from '../attributes';
import { serializeAttributes } from '../attributes';
import { getMainCarrier } from '../carrier';
import { getCurrentScope } from '../currentScopes';
import {
Expand All @@ -12,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 { 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';
Expand Down Expand Up @@ -105,6 +115,27 @@ 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 getStreamedSpanLinks(
links?: SpanLink[],
): SpanLinkJSON<RawAttributes<Record<string, unknown>>>[] | 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,
...restContext,
}));
} else {
return undefined;
}
}

/**
* Convert a span time input into a timestamp in seconds.
*/
Expand Down Expand Up @@ -150,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,
Expand All @@ -187,6 +207,77 @@ export function spanToJSON(span: Span): SpanJSON {
};
}

/**
* Convert a span to the intermediate {@link StreamedSpanJSON} representation.
*/
export function spanToStreamedSpanJSON(span: Span): StreamedSpanJSON {
if (spanIsSentrySpan(span)) {
return span.getStreamedSpanJSON();
}

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;

return {
name,
span_id,
trace_id,
parent_span_id: getOtelParentSpanId(span),
start_timestamp: spanTimeInputToSeconds(startTime),
end_timestamp: spanTimeInputToSeconds(endTime),
is_segment: span === INTERNAL_getSegmentSpan(span),
status: getSimpleStatusMessage(status),
attributes,
links: getStreamedSpanLinks(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),
};
}

/**
* 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.
* The returned serilaized spans must not be consumed by users or SDK integrations.
*/
export function streamedSpanJsonToSerializedSpan(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<OpenTelemetrySdkTraceBaseSpan>;
return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status;
Expand Down Expand Up @@ -237,6 +328,13 @@ export function getStatusMessage(status: SpanStatus | undefined): string | undef
return status.message || 'internal_error';
}

/**
* Convert the various statuses to the simple onces expected by Sentry for steamed spans ('ok' is default).
*/
export function getSimpleStatusMessage(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';

Expand Down Expand Up @@ -298,7 +396,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;
}

Expand Down
Loading
Loading