Skip to content

Commit bfe7bfa

Browse files
authored
feat(core): Create types and utilities for span links (#15375)
This is the base for adding span links to the SDK. Those types will be used both our OTel and Core SDK.
1 parent a31e0d3 commit bfe7bfa

File tree

7 files changed

+180
-32
lines changed

7 files changed

+180
-32
lines changed

packages/core/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ export { handleCallbackErrors } from './utils/handleCallbackErrors';
7676
export { parameterize } from './utils/parameterize';
7777
export { addAutoIpAddressToSession, addAutoIpAddressToUser } from './utils/ipAddress';
7878
export {
79+
convertSpanLinksForEnvelope,
7980
spanToTraceHeader,
8081
spanToJSON,
8182
spanIsSampled,

packages/core/src/tracing/sentryNonRecordingSpan.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -69,24 +69,12 @@ export class SentryNonRecordingSpan implements Span {
6969
return this;
7070
}
7171

72-
/**
73-
* This should generally not be used,
74-
* but we need it for being compliant with the OTEL Span interface.
75-
*
76-
* @hidden
77-
* @internal
78-
*/
72+
/** @inheritDoc */
7973
public addLink(_link: unknown): this {
8074
return this;
8175
}
8276

83-
/**
84-
* This should generally not be used,
85-
* but we need it for being compliant with the OTEL Span interface.
86-
*
87-
* @hidden
88-
* @internal
89-
*/
77+
/** @inheritDoc */
9078
public addLinks(_links: unknown[]): this {
9179
return this;
9280
}

packages/core/src/tracing/sentrySpan.ts

+2-14
Original file line numberDiff line numberDiff line change
@@ -109,24 +109,12 @@ export class SentrySpan implements Span {
109109
}
110110
}
111111

112-
/**
113-
* This should generally not be used,
114-
* but it is needed for being compliant with the OTEL Span interface.
115-
*
116-
* @hidden
117-
* @internal
118-
*/
112+
/** @inheritDoc */
119113
public addLink(_link: unknown): this {
120114
return this;
121115
}
122116

123-
/**
124-
* This should generally not be used,
125-
* but it is needed for being compliant with the OTEL Span interface.
126-
*
127-
* @hidden
128-
* @internal
129-
*/
117+
/** @inheritDoc */
130118
public addLinks(_links: unknown[]): this {
131119
return this;
132120
}

packages/core/src/types-hoist/link.ts

+30
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type { SpanAttributeValue, SpanContextData } from './span';
2+
3+
type SpanLinkAttributes = {
4+
/**
5+
* Setting the link type to 'previous_trace' helps the Sentry product linking to the previous trace
6+
*/
7+
'sentry.link.type'?: string | 'previous_trace';
8+
} & Record<string, SpanAttributeValue | undefined>;
9+
10+
export interface SpanLink {
11+
/**
12+
* Contains the SpanContext of the span to link to
13+
*/
14+
context: SpanContextData;
15+
/**
16+
* A key-value pair with primitive values or an array of primitive values
17+
*/
18+
attributes?: SpanLinkAttributes;
19+
}
20+
21+
/**
22+
* Link interface for the event envelope item. It's a flattened representation of `SpanLink`.
23+
* Can include additional fields defined by OTel.
24+
*/
25+
export interface SpanLinkJSON extends Record<string, unknown> {
26+
span_id: string;
27+
trace_id: string;
28+
sampled?: boolean;
29+
attributes?: SpanLinkAttributes;
30+
}

packages/core/src/types-hoist/span.ts

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { SpanLink, SpanLinkJSON } from './link';
12
import type { Measurements } from './measurement';
23
import type { HrTime } from './opentelemetry';
34
import type { SpanStatus } from './spanStatus';
@@ -50,6 +51,7 @@ export interface SpanJSON {
5051
measurements?: Measurements;
5152
is_segment?: boolean;
5253
segment_id?: string;
54+
links?: SpanLinkJSON[];
5355
}
5456

5557
// These are aligned with OpenTelemetry trace flags
@@ -249,14 +251,21 @@ export interface Span {
249251
addEvent(name: string, attributesOrStartTime?: SpanAttributes | SpanTimeInput, startTime?: SpanTimeInput): this;
250252

251253
/**
252-
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
254+
* Associates this span with a related span. Links can reference spans from the same or different trace
255+
* and are typically used for batch operations, cross-trace scenarios, or scatter/gather patterns.
256+
*
257+
* Prefer setting links directly when starting a span (e.g. `Sentry.startSpan()`) as some context information is only available during span creation.
258+
* @param link - The link containing the context of the span to link to and optional attributes
253259
*/
254-
addLink(link: unknown): this;
260+
addLink(link: SpanLink): this;
255261

256262
/**
257-
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface
263+
* Associates this span with multiple related spans. See {@link addLink} for more details.
264+
*
265+
* Prefer setting links directly when starting a span (e.g. `Sentry.startSpan()`) as some context information is only available during span creation.
266+
* @param links - Array of links to associate with this span
258267
*/
259-
addLinks(links: unknown): this;
268+
addLinks(links: SpanLink[]): this;
260269

261270
/**
262271
* NOT USED IN SENTRY, only added for compliance with OTEL Span interface

packages/core/src/utils/spanUtils.ts

+20
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
SpanTimeInput,
2020
TraceContext,
2121
} from '../types-hoist';
22+
import type { SpanLink, SpanLinkJSON } from '../types-hoist/link';
2223
import { consoleSandbox } from '../utils-hoist/logger';
2324
import { addNonEnumerableProperty, dropUndefinedKeys } from '../utils-hoist/object';
2425
import { generateSpanId } from '../utils-hoist/propagationContext';
@@ -81,6 +82,25 @@ export function spanToTraceHeader(span: Span): string {
8182
return generateSentryTraceHeader(traceId, spanId, sampled);
8283
}
8384

85+
/**
86+
* Converts the span links array to a flattened version to be sent within an envelope.
87+
*
88+
* If the links array is empty, it returns `undefined` so the empty value can be dropped before it's sent.
89+
*/
90+
export function convertSpanLinksForEnvelope(links?: SpanLink[]): SpanLinkJSON[] | undefined {
91+
if (links && links.length > 0) {
92+
return links.map(({ context: { spanId, traceId, traceFlags, ...restContext }, attributes }) => ({
93+
span_id: spanId,
94+
trace_id: traceId,
95+
sampled: traceFlags === TRACE_FLAG_SAMPLED,
96+
attributes,
97+
...restContext,
98+
}));
99+
} else {
100+
return undefined;
101+
}
102+
}
103+
84104
/**
85105
* Convert a span time input into a timestamp in seconds.
86106
*/

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

+112
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@ import {
77
SPAN_STATUS_UNSET,
88
SentrySpan,
99
TRACEPARENT_REGEXP,
10+
convertSpanLinksForEnvelope,
1011
setCurrentClient,
1112
spanToTraceHeader,
1213
startInactiveSpan,
1314
startSpan,
1415
timestampInSeconds,
1516
} from '../../../src';
1617
import type { Span, SpanAttributes, SpanStatus, SpanTimeInput } from '../../../src/types-hoist';
18+
import type { SpanLink } from '../../../src/types-hoist/link';
1719
import type { OpenTelemetrySdkTraceBaseSpan } from '../../../src/utils/spanUtils';
20+
import { TRACE_FLAG_NONE, TRACE_FLAG_SAMPLED } from '../../../src/utils/spanUtils';
1821
import {
1922
getRootSpan,
2023
spanIsSampled,
@@ -156,6 +159,115 @@ describe('spanToTraceContext', () => {
156159
});
157160
});
158161

162+
describe('convertSpanLinksForEnvelope', () => {
163+
it('returns undefined for undefined input', () => {
164+
expect(convertSpanLinksForEnvelope(undefined)).toBeUndefined();
165+
});
166+
167+
it('returns undefined for empty array input', () => {
168+
expect(convertSpanLinksForEnvelope([])).toBeUndefined();
169+
});
170+
171+
it('converts a single span link to a flattened envelope item', () => {
172+
const links: SpanLink[] = [
173+
{
174+
context: {
175+
spanId: 'span1',
176+
traceId: 'trace1',
177+
traceFlags: TRACE_FLAG_SAMPLED,
178+
},
179+
attributes: {
180+
'sentry.link.type': 'previous_trace',
181+
},
182+
},
183+
];
184+
185+
const result = convertSpanLinksForEnvelope(links);
186+
187+
result?.forEach(item => expect(item).not.toHaveProperty('context'));
188+
expect(result).toEqual([
189+
{
190+
span_id: 'span1',
191+
trace_id: 'trace1',
192+
sampled: true,
193+
attributes: {
194+
'sentry.link.type': 'previous_trace',
195+
},
196+
},
197+
]);
198+
});
199+
200+
it('converts multiple span links to a flattened envelope item', () => {
201+
const links: SpanLink[] = [
202+
{
203+
context: {
204+
spanId: 'span1',
205+
traceId: 'trace1',
206+
traceFlags: TRACE_FLAG_SAMPLED,
207+
},
208+
attributes: {
209+
'sentry.link.type': 'previous_trace',
210+
},
211+
},
212+
{
213+
context: {
214+
spanId: 'span2',
215+
traceId: 'trace2',
216+
traceFlags: TRACE_FLAG_NONE,
217+
},
218+
attributes: {
219+
'sentry.link.type': 'another_trace',
220+
},
221+
},
222+
];
223+
224+
const result = convertSpanLinksForEnvelope(links);
225+
226+
result?.forEach(item => expect(item).not.toHaveProperty('context'));
227+
expect(result).toEqual([
228+
{
229+
span_id: 'span1',
230+
trace_id: 'trace1',
231+
sampled: true,
232+
attributes: {
233+
'sentry.link.type': 'previous_trace',
234+
},
235+
},
236+
{
237+
span_id: 'span2',
238+
trace_id: 'trace2',
239+
sampled: false,
240+
attributes: {
241+
'sentry.link.type': 'another_trace',
242+
},
243+
},
244+
]);
245+
});
246+
247+
it('handles span links without attributes', () => {
248+
const links: SpanLink[] = [
249+
{
250+
context: {
251+
spanId: 'span1',
252+
traceId: 'trace1',
253+
traceFlags: TRACE_FLAG_SAMPLED,
254+
},
255+
},
256+
];
257+
258+
const result = convertSpanLinksForEnvelope(links);
259+
260+
result?.forEach(item => expect(item).not.toHaveProperty('context'));
261+
expect(result).toEqual([
262+
{
263+
span_id: 'span1',
264+
trace_id: 'trace1',
265+
sampled: true,
266+
},
267+
]);
268+
});
269+
});
270+
159271
describe('spanTimeInputToSeconds', () => {
160272
it('works with undefined', () => {
161273
const now = timestampInSeconds();

0 commit comments

Comments
 (0)