Skip to content

Commit 5aac890

Browse files
authored
feat(core): Add spanToJSON() method to get span properties (#10074)
This is supposed to be an internal API and not necessarily to be used by users. Naming wise, it's a bit tricky... I went with `JSON` to make it very clear what this is for, but 🤷
1 parent 3fc7916 commit 5aac890

File tree

11 files changed

+167
-40
lines changed

11 files changed

+167
-40
lines changed

packages/core/package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,5 +56,12 @@
5656
"volta": {
5757
"extends": "../../package.json"
5858
},
59+
"madge": {
60+
"detectiveOptions": {
61+
"ts": {
62+
"skipTypeImports": true
63+
}
64+
}
65+
},
5966
"sideEffects": false
6067
}

packages/core/src/index.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,10 @@ export { createCheckInEnvelope } from './checkin';
7575
export { hasTracingEnabled } from './utils/hasTracingEnabled';
7676
export { isSentryRequestUrl } from './utils/isSentryRequestUrl';
7777
export { handleCallbackErrors } from './utils/handleCallbackErrors';
78-
export { spanToTraceHeader } from './utils/spanUtils';
78+
export {
79+
spanToTraceHeader,
80+
spanToJSON,
81+
} from './utils/spanUtils';
7982
export { DEFAULT_ENVIRONMENT } from './constants';
8083
export { ModuleMetadata } from './integrations/metadata';
8184
export { RequestData } from './integrations/requestdata';

packages/core/src/tracing/span.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {
66
SpanAttributeValue,
77
SpanAttributes,
88
SpanContext,
9+
SpanJSON,
910
SpanOrigin,
1011
SpanTimeInput,
1112
TraceContext,
@@ -372,22 +373,9 @@ export class Span implements SpanInterface {
372373
}
373374

374375
/**
375-
* @inheritDoc
376+
* Get JSON representation of this span.
376377
*/
377-
public toJSON(): {
378-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
379-
data?: { [key: string]: any };
380-
description?: string;
381-
op?: string;
382-
parent_span_id?: string;
383-
span_id: string;
384-
start_timestamp: number;
385-
status?: string;
386-
tags?: { [key: string]: Primitive };
387-
timestamp?: number;
388-
trace_id: string;
389-
origin?: SpanOrigin;
390-
} {
378+
public getSpanJSON(): SpanJSON {
391379
return dropUndefinedKeys({
392380
data: this._getData(),
393381
description: this.description,
@@ -408,6 +396,14 @@ export class Span implements SpanInterface {
408396
return !this.endTimestamp && !!this.sampled;
409397
}
410398

399+
/**
400+
* Convert the object to JSON.
401+
* @deprecated Use `spanToJSON(span)` instead.
402+
*/
403+
public toJSON(): SpanJSON {
404+
return this.getSpanJSON();
405+
}
406+
411407
/**
412408
* Get the merged data for this span.
413409
* For now, this combines `data` and `attributes` together,

packages/core/src/tracing/transaction.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
285285
// We don't want to override trace context
286286
trace: spanToTraceContext(this),
287287
},
288+
// TODO: Pass spans serialized via `spanToJSON()` here instead in v8.
288289
spans: finishedSpans,
289290
start_timestamp: this.startTimestamp,
290291
tags: this.tags,

packages/core/src/utils/spanUtils.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
1-
import type { Span, SpanTimeInput, TraceContext } from '@sentry/types';
1+
import type { Span, SpanJSON, SpanTimeInput, TraceContext } from '@sentry/types';
22
import { dropUndefinedKeys, generateSentryTraceHeader, timestampInSeconds } from '@sentry/utils';
3+
import type { Span as SpanClass } from '../tracing/span';
34

45
/**
56
* Convert a span to a trace context, which can be sent as the `trace` context in an event.
67
*/
78
export function spanToTraceContext(span: Span): TraceContext {
8-
const { data, description, op, parent_span_id, span_id, status, tags, trace_id, origin } = span.toJSON();
9+
const { spanId: span_id, traceId: trace_id } = span;
10+
const { data, description, op, parent_span_id, status, tags, origin } = spanToJSON(span);
911

1012
return dropUndefinedKeys({
1113
data,
@@ -54,3 +56,35 @@ function ensureTimestampInSeconds(timestamp: number): number {
5456
const isMs = timestamp > 9999999999;
5557
return isMs ? timestamp / 1000 : timestamp;
5658
}
59+
60+
/**
61+
* Convert a span to a JSON representation.
62+
* Note that all fields returned here are optional and need to be guarded against.
63+
*
64+
* Note: Because of this, we currently have a circular type dependency (which we opted out of in package.json).
65+
* This is not avoidable as we need `spanToJSON` in `spanUtils.ts`, which in turn is needed by `span.ts` for backwards compatibility.
66+
* And `spanToJSON` needs the Span class from `span.ts` to check here.
67+
* TODO v8: When we remove the deprecated stuff from `span.ts`, we can remove the circular dependency again.
68+
*/
69+
export function spanToJSON(span: Span): Partial<SpanJSON> {
70+
if (spanIsSpanClass(span)) {
71+
return span.getSpanJSON();
72+
}
73+
74+
// Fallback: We also check for `.toJSON()` here...
75+
// eslint-disable-next-line deprecation/deprecation
76+
if (typeof span.toJSON === 'function') {
77+
// eslint-disable-next-line deprecation/deprecation
78+
return span.toJSON();
79+
}
80+
81+
return {};
82+
}
83+
84+
/**
85+
* Sadly, due to circular dependency checks we cannot actually import the Span class here and check for instanceof.
86+
* :( So instead we approximate this by checking if it has the `getSpanJSON` method.
87+
*/
88+
function spanIsSpanClass(span: Span): span is SpanClass {
89+
return typeof (span as SpanClass).getSpanJSON === 'function';
90+
}

packages/core/test/lib/utils/spanUtils.test.ts

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { TRACEPARENT_REGEXP, timestampInSeconds } from '@sentry/utils';
22
import { Span, spanToTraceHeader } from '../../../src';
3-
import { spanTimeInputToSeconds } from '../../../src/utils/spanUtils';
3+
import { spanTimeInputToSeconds, spanToJSON } from '../../../src/utils/spanUtils';
44

55
describe('spanToTraceHeader', () => {
66
test('simple', () => {
@@ -46,3 +46,72 @@ describe('spanTimeInputToSeconds', () => {
4646
expect(spanTimeInputToSeconds(timestamp)).toEqual(seconds + 0.000009);
4747
});
4848
});
49+
50+
describe('spanToJSON', () => {
51+
it('works with a simple span', () => {
52+
const span = new Span();
53+
expect(spanToJSON(span)).toEqual({
54+
span_id: span.spanId,
55+
trace_id: span.traceId,
56+
origin: 'manual',
57+
start_timestamp: span.startTimestamp,
58+
});
59+
});
60+
61+
it('works with a full span', () => {
62+
const span = new Span({
63+
name: 'test name',
64+
op: 'test op',
65+
parentSpanId: '1234',
66+
spanId: '5678',
67+
status: 'ok',
68+
tags: {
69+
foo: 'bar',
70+
},
71+
traceId: 'abcd',
72+
origin: 'auto',
73+
startTimestamp: 123,
74+
});
75+
76+
expect(spanToJSON(span)).toEqual({
77+
description: 'test name',
78+
op: 'test op',
79+
parent_span_id: '1234',
80+
span_id: '5678',
81+
status: 'ok',
82+
tags: {
83+
foo: 'bar',
84+
},
85+
trace_id: 'abcd',
86+
origin: 'auto',
87+
start_timestamp: 123,
88+
});
89+
});
90+
91+
it('works with a custom class without spanToJSON', () => {
92+
const span = {
93+
toJSON: () => {
94+
return {
95+
span_id: 'span_id',
96+
trace_id: 'trace_id',
97+
origin: 'manual',
98+
start_timestamp: 123,
99+
};
100+
},
101+
} as unknown as Span;
102+
103+
expect(spanToJSON(span)).toEqual({
104+
span_id: 'span_id',
105+
trace_id: 'trace_id',
106+
origin: 'manual',
107+
start_timestamp: 123,
108+
});
109+
});
110+
111+
it('returns empty object if span does not have getter methods', () => {
112+
// eslint-disable-next-line
113+
const span = new Span().toJSON();
114+
115+
expect(spanToJSON(span as unknown as Span)).toEqual({});
116+
});
117+
});

packages/node-experimental/test/integration/transactions.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SpanKind, TraceFlags, context, trace } from '@opentelemetry/api';
22
import type { SpanProcessor } from '@opentelemetry/sdk-trace-base';
33
import { SemanticAttributes } from '@opentelemetry/semantic-conventions';
4+
import { spanToJSON } from '@sentry/core';
45
import { SentrySpanProcessor, getCurrentHub, setPropagationContextOnContext } from '@sentry/opentelemetry';
56
import type { Integration, PropagationContext, TransactionEvent } from '@sentry/types';
67
import { logger } from '@sentry/utils';
@@ -145,7 +146,7 @@ describe('Integration | Transactions', () => {
145146

146147
// note: Currently, spans do not have any context/span added to them
147148
// This is the same behavior as for the "regular" SDKs
148-
expect(spans.map(span => span.toJSON())).toEqual([
149+
expect(spans.map(span => spanToJSON(span))).toEqual([
149150
{
150151
data: { 'otel.kind': 'INTERNAL' },
151152
description: 'inner span 1',
@@ -399,7 +400,7 @@ describe('Integration | Transactions', () => {
399400

400401
// note: Currently, spans do not have any context/span added to them
401402
// This is the same behavior as for the "regular" SDKs
402-
expect(spans.map(span => span.toJSON())).toEqual([
403+
expect(spans.map(span => spanToJSON(span))).toEqual([
403404
{
404405
data: { 'otel.kind': 'INTERNAL' },
405406
description: 'inner span 1',

packages/opentelemetry/test/custom/transaction.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { spanToJSON } from '@sentry/core';
12
import { getCurrentHub } from '../../src/custom/hub';
23
import { OpenTelemetryScope } from '../../src/custom/scope';
34
import { OpenTelemetryTransaction, startTransaction } from '../../src/custom/transaction';
@@ -157,7 +158,7 @@ describe('startTranscation', () => {
157158
spanMetadata: {},
158159
});
159160

160-
expect(transaction.toJSON()).toEqual(
161+
expect(spanToJSON(transaction)).toEqual(
161162
expect.objectContaining({
162163
origin: 'manual',
163164
span_id: expect.any(String),
@@ -186,7 +187,7 @@ describe('startTranscation', () => {
186187
spanMetadata: {},
187188
});
188189

189-
expect(transaction.toJSON()).toEqual(
190+
expect(spanToJSON(transaction)).toEqual(
190191
expect.objectContaining({
191192
origin: 'manual',
192193
span_id: 'span1',

packages/opentelemetry/test/integration/transactions.test.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { addBreadcrumb, setTag } from '@sentry/core';
44
import type { PropagationContext, TransactionEvent } from '@sentry/types';
55
import { logger } from '@sentry/utils';
66

7+
import { spanToJSON } from '@sentry/core';
78
import { getCurrentHub } from '../../src/custom/hub';
89
import { SentrySpanProcessor } from '../../src/spanProcessor';
910
import { startInactiveSpan, startSpan } from '../../src/trace';
@@ -142,7 +143,7 @@ describe('Integration | Transactions', () => {
142143

143144
// note: Currently, spans do not have any context/span added to them
144145
// This is the same behavior as for the "regular" SDKs
145-
expect(spans.map(span => span.toJSON())).toEqual([
146+
expect(spans.map(span => spanToJSON(span))).toEqual([
146147
{
147148
data: { 'otel.kind': 'INTERNAL' },
148149
description: 'inner span 1',
@@ -393,7 +394,7 @@ describe('Integration | Transactions', () => {
393394

394395
// note: Currently, spans do not have any context/span added to them
395396
// This is the same behavior as for the "regular" SDKs
396-
expect(spans.map(span => span.toJSON())).toEqual([
397+
expect(spans.map(span => spanToJSON(span))).toEqual([
397398
{
398399
data: { 'otel.kind': 'INTERNAL' },
399400
description: 'inner span 1',

packages/types/src/index.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,15 @@ export type {
8989

9090
// eslint-disable-next-line deprecation/deprecation
9191
export type { Severity, SeverityLevel } from './severity';
92-
export type { Span, SpanContext, SpanOrigin, SpanAttributeValue, SpanAttributes, SpanTimeInput } from './span';
92+
export type {
93+
Span,
94+
SpanContext,
95+
SpanOrigin,
96+
SpanAttributeValue,
97+
SpanAttributes,
98+
SpanTimeInput,
99+
SpanJSON,
100+
} from './span';
93101
export type { StackFrame } from './stackframe';
94102
export type { Stacktrace, StackParser, StackLineParser, StackLineParserFn } from './stacktrace';
95103
export type { TextEncoderInternal } from './textencoder';

packages/types/src/span.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ export type SpanAttributes = Record<string, SpanAttributeValue | undefined>;
2828
/** This type is aligned with the OpenTelemetry TimeInput type. */
2929
export type SpanTimeInput = HrTime | number | Date;
3030

31+
/** A JSON representation of a span. */
32+
export interface SpanJSON {
33+
data?: { [key: string]: any };
34+
description?: string;
35+
op?: string;
36+
parent_span_id?: string;
37+
span_id: string;
38+
start_timestamp: number;
39+
status?: string;
40+
tags?: { [key: string]: Primitive };
41+
timestamp?: number;
42+
trace_id: string;
43+
origin?: SpanOrigin;
44+
}
45+
3146
/** Interface holding all properties that can be set on a Span on creation. */
3247
export interface SpanContext {
3348
/**
@@ -256,20 +271,11 @@ export interface Span extends SpanContext {
256271
*/
257272
getTraceContext(): TraceContext;
258273

259-
/** Convert the object to JSON */
260-
toJSON(): {
261-
data?: { [key: string]: any };
262-
description?: string;
263-
op?: string;
264-
parent_span_id?: string;
265-
span_id: string;
266-
start_timestamp: number;
267-
status?: string;
268-
tags?: { [key: string]: Primitive };
269-
timestamp?: number;
270-
trace_id: string;
271-
origin?: SpanOrigin;
272-
};
274+
/**
275+
* Convert the object to JSON.
276+
* @deprecated Use `spanToJSON(span)` instead.
277+
*/
278+
toJSON(): SpanJSON;
273279

274280
/**
275281
* If this is span is actually recording data.

0 commit comments

Comments
 (0)