Skip to content

Commit 7e66a04

Browse files
authored
feat(core): Update spanToJSON to handle OTEL spans (#10922)
This is an important step towards aligning OTEL & Sentry Spans 🎉 You can see that the `Span` interface is now aligned enough that it accepts an OTEL span.
1 parent 8ac4d73 commit 7e66a04

File tree

9 files changed

+299
-94
lines changed

9 files changed

+299
-94
lines changed

packages/angular/test/tracing.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Component } from '@angular/core';
22
import type { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
3-
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE } from '@sentry/core';
3+
import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON } from '@sentry/core';
44

55
import { TraceClassDecorator, TraceDirective, TraceMethodDecorator, instrumentAngularRouting } from '../src';
66
import { getParameterizedRouteFromSnapshot } from '../src/tracing';
@@ -13,7 +13,7 @@ const defaultStartTransaction = (ctx: any) => {
1313
...ctx,
1414
updateName: jest.fn(name => (transaction.name = name)),
1515
setAttribute: jest.fn(),
16-
toJSON: () => ({
16+
getSpanJSON: () => ({
1717
data: {
1818
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
1919
...ctx.data,
@@ -117,7 +117,7 @@ describe('Angular Tracing', () => {
117117
const customStartTransaction = jest.fn((ctx: any) => {
118118
transaction = {
119119
...ctx,
120-
toJSON: () => ({
120+
getSpanJSON: () => ({
121121
data: {
122122
...ctx.data,
123123
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom',
@@ -154,7 +154,7 @@ describe('Angular Tracing', () => {
154154

155155
expect(transaction.updateName).toHaveBeenCalledTimes(0);
156156
expect(transaction.name).toEqual(url);
157-
expect(transaction.toJSON().data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' });
157+
expect(spanToJSON(transaction).data).toEqual({ [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'custom' });
158158

159159
env.destroy();
160160
});

packages/core/src/tracing/sentrySpan.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,11 @@ import { getRootSpan } from '../utils/getRootSpan';
2121
import {
2222
TRACE_FLAG_NONE,
2323
TRACE_FLAG_SAMPLED,
24+
getStatusMessage,
2425
spanTimeInputToSeconds,
2526
spanToJSON,
2627
spanToTraceContext,
2728
} from '../utils/spanUtils';
28-
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from './spanstatus';
2929
import { addChildSpanToSpan } from './utils';
3030

3131
/**
@@ -500,15 +500,3 @@ export class SentrySpan implements Span {
500500
return hasData ? data : attributes;
501501
}
502502
}
503-
504-
function getStatusMessage(status: SpanStatus | undefined): string | undefined {
505-
if (!status || status.code === SPAN_STATUS_UNSET) {
506-
return undefined;
507-
}
508-
509-
if (status.code === SPAN_STATUS_OK) {
510-
return 'ok';
511-
}
512-
513-
return status.message || 'unknown_error';
514-
}

packages/core/src/tracing/transaction.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
Hub,
66
MeasurementUnit,
77
Measurements,
8+
SpanJSON,
89
SpanTimeInput,
910
Transaction as TransactionInterface,
1011
TransactionContext,
@@ -253,8 +254,8 @@ export class Transaction extends SentrySpan implements TransactionInterface {
253254
return undefined;
254255
}
255256

256-
// We only want to include finished spans in the event
257-
const finishedSpans = getSpanTree(this).filter(span => span !== this && spanToJSON(span).timestamp);
257+
// The transaction span itself should be filtered out
258+
const finishedSpans = getSpanTree(this).filter(span => span !== this);
258259

259260
if (this._trimEnd && finishedSpans.length > 0) {
260261
const endTimes = finishedSpans.map(span => spanToJSON(span).timestamp).filter(Boolean) as number[];
@@ -263,6 +264,13 @@ export class Transaction extends SentrySpan implements TransactionInterface {
263264
});
264265
}
265266

267+
// We want to filter out any incomplete SpanJSON objects
268+
function isFullFinishedSpan(input: Partial<SpanJSON>): input is SpanJSON {
269+
return !!input.start_timestamp && !!input.timestamp && !!input.span_id && !!input.trace_id;
270+
}
271+
272+
const spans = finishedSpans.map(span => spanToJSON(span)).filter(isFullFinishedSpan);
273+
266274
const { scope: capturedSpanScope, isolationScope: capturedSpanIsolationScope } = getCapturedScopesOnSpan(this);
267275

268276
// eslint-disable-next-line deprecation/deprecation
@@ -276,7 +284,7 @@ export class Transaction extends SentrySpan implements TransactionInterface {
276284
// We don't want to override trace context
277285
trace: spanToTraceContext(this),
278286
},
279-
spans: finishedSpans.map(span => spanToJSON(span)),
287+
spans,
280288
start_timestamp: this._startTime,
281289
// eslint-disable-next-line deprecation/deprecation
282290
tags: this.tags,

packages/core/src/utils/spanUtils.ts

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
1-
import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types';
1+
import type {
2+
Span,
3+
SpanAttributes,
4+
SpanJSON,
5+
SpanOrigin,
6+
SpanStatus,
7+
SpanTimeInput,
8+
TraceContext,
9+
} from '@sentry/types';
210
import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils';
11+
import { getMetricSummaryJsonForSpan } from '../metrics/metric-summary';
12+
import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../semanticAttributes';
13+
import { SPAN_STATUS_OK, SPAN_STATUS_UNSET } from '../tracing';
314
import type { SentrySpan } from '../tracing/sentrySpan';
415

516
// These are aligned with OpenTelemetry trace flags
@@ -62,8 +73,6 @@ function ensureTimestampInSeconds(timestamp: number): number {
6273
return isMs ? timestamp / 1000 : timestamp;
6374
}
6475

65-
type SpanWithToJSON = Span & { toJSON: () => SpanJSON };
66-
6776
/**
6877
* Convert a span to a JSON representation.
6978
* Note that all fields returned here are optional and need to be guarded against.
@@ -77,14 +86,52 @@ export function spanToJSON(span: Span): Partial<SpanJSON> {
7786
return span.getSpanJSON();
7887
}
7988

80-
// Fallback: We also check for `.toJSON()` here...
81-
if (typeof (span as SpanWithToJSON).toJSON === 'function') {
82-
return (span as SpanWithToJSON).toJSON();
89+
try {
90+
const { spanId: span_id, traceId: trace_id } = span.spanContext();
91+
92+
// Handle a span from @opentelemetry/sdk-base-trace's `Span` class
93+
if (spanIsOpenTelemetrySdkTraceBaseSpan(span)) {
94+
const { attributes, startTime, name, endTime, parentSpanId, status } = span;
95+
96+
return dropUndefinedKeys({
97+
span_id,
98+
trace_id,
99+
data: attributes,
100+
description: name,
101+
parent_span_id: parentSpanId,
102+
start_timestamp: spanTimeInputToSeconds(startTime),
103+
// This is [0,0] by default in OTEL, in which case we want to interpret this as no end time
104+
timestamp: spanTimeInputToSeconds(endTime) || undefined,
105+
status: getStatusMessage(status),
106+
op: attributes[SEMANTIC_ATTRIBUTE_SENTRY_OP],
107+
origin: attributes[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN] as SpanOrigin | undefined,
108+
_metrics_summary: getMetricSummaryJsonForSpan(span),
109+
});
110+
}
111+
112+
// Finally, at least we have `spanContext()`....
113+
return {
114+
span_id,
115+
trace_id,
116+
};
117+
} catch {
118+
return {};
83119
}
120+
}
84121

85-
// TODO: Also handle OTEL spans here!
122+
function spanIsOpenTelemetrySdkTraceBaseSpan(span: Span): span is OpenTelemetrySdkTraceBaseSpan {
123+
const castSpan = span as OpenTelemetrySdkTraceBaseSpan;
124+
return !!castSpan.attributes && !!castSpan.startTime && !!castSpan.name && !!castSpan.endTime && !!castSpan.status;
125+
}
86126

87-
return {};
127+
/** Exported only for tests. */
128+
export interface OpenTelemetrySdkTraceBaseSpan extends Span {
129+
attributes: SpanAttributes;
130+
startTime: SpanTimeInput;
131+
name: string;
132+
status: SpanStatus;
133+
endTime: SpanTimeInput;
134+
parentSpanId?: string;
88135
}
89136

90137
/**
@@ -108,3 +155,16 @@ export function spanIsSampled(span: Span): boolean {
108155
// eslint-disable-next-line no-bitwise
109156
return Boolean(traceFlags & TRACE_FLAG_SAMPLED);
110157
}
158+
159+
/** Get the status message to use for a JSON representation of a span. */
160+
export function getStatusMessage(status: SpanStatus | undefined): string | undefined {
161+
if (!status || status.code === SPAN_STATUS_UNSET) {
162+
return undefined;
163+
}
164+
165+
if (status.code === SPAN_STATUS_OK) {
166+
return 'ok';
167+
}
168+
169+
return status.message || 'unknown_error';
170+
}

0 commit comments

Comments
 (0)