Skip to content

Commit 6363e75

Browse files
authored
feat(opentelemetry): Add links to span options (#15403)
This PR adds the possibility to add a `links` array that can be passed to the span options at creation of the span.
1 parent abb37a3 commit 6363e75

File tree

7 files changed

+216
-1
lines changed

7 files changed

+216
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
2+
import * as Sentry from '@sentry/node';
3+
4+
Sentry.init({
5+
dsn: 'https://[email protected]/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
integrations: [],
9+
transport: loggingTransport,
10+
});
11+
12+
const parentSpan1 = Sentry.startInactiveSpan({ name: 'parent1' });
13+
parentSpan1.end();
14+
15+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
16+
Sentry.startSpan(
17+
{
18+
name: 'parent2',
19+
links: [{ context: parentSpan1.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }],
20+
},
21+
async () => {
22+
// eslint-disable-next-line @typescript-eslint/no-floating-promises
23+
Sentry.startSpan({ name: 'child2.1' }, async childSpan1 => {
24+
childSpan1.end();
25+
});
26+
},
27+
);

dev-packages/node-integration-tests/suites/tracing/linking/test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
import { createRunner } from '../../../utils/runner';
22

33
describe('span links', () => {
4+
test('should link spans by adding "links" to span options', done => {
5+
let span1_traceId: string, span1_spanId: string;
6+
7+
createRunner(__dirname, 'scenario-span-options.ts')
8+
.expect({
9+
transaction: event => {
10+
expect(event.transaction).toBe('parent1');
11+
12+
const traceContext = event.contexts?.trace;
13+
span1_traceId = traceContext?.trace_id as string;
14+
span1_spanId = traceContext?.span_id as string;
15+
},
16+
})
17+
.expect({
18+
transaction: event => {
19+
expect(event.transaction).toBe('parent2');
20+
21+
const traceContext = event.contexts?.trace;
22+
expect(traceContext).toBeDefined();
23+
expect(traceContext?.links).toEqual([
24+
expect.objectContaining({
25+
trace_id: expect.stringMatching(span1_traceId),
26+
span_id: expect.stringMatching(span1_spanId),
27+
}),
28+
]);
29+
},
30+
})
31+
.start(done);
32+
});
33+
434
test('should link spans with addLink() in trace context', done => {
535
let span1_traceId: string, span1_spanId: string;
636

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

+6
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,12 @@ export interface SentrySpanArguments {
182182
*/
183183
endTimestamp?: number | undefined;
184184

185+
/**
186+
* Links to associate with the new span. Setting links here is preferred over addLink()
187+
* as certain context information is only available during span creation.
188+
*/
189+
links?: SpanLink[];
190+
185191
/**
186192
* Set to `true` if this span should be sent as a standalone segment span
187193
* as opposed to a transaction.

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

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Scope } from '../scope';
2+
import type { SpanLink } from './link';
23
import type { Span, SpanAttributes, SpanTimeInput } from './span';
34

45
export interface StartSpanOptions {
@@ -44,6 +45,12 @@ export interface StartSpanOptions {
4445
/** Attributes for the span. */
4546
attributes?: SpanAttributes;
4647

48+
/**
49+
* Links to associate with the new span. Setting links here is preferred over addLink()
50+
* as it allows sampling decisions to consider the link information.
51+
*/
52+
links?: SpanLink[];
53+
4754
/**
4855
* Experimental options without any stability guarantees. Use with caution!
4956
*/

packages/opentelemetry/src/trace.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ function getTracer(): Tracer {
160160
}
161161

162162
function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions {
163-
const { startTime, attributes, kind, op } = options;
163+
const { startTime, attributes, kind, op, links } = options;
164164

165165
// OTEL expects timestamps in ms, not seconds
166166
const fixedStartTime = typeof startTime === 'number' ? ensureTimestampInMilliseconds(startTime) : startTime;
@@ -173,6 +173,7 @@ function getSpanOptions(options: OpenTelemetrySpanContext): SpanOptions {
173173
}
174174
: attributes,
175175
kind,
176+
links,
176177
startTime: fixedStartTime,
177178
};
178179
}

packages/opentelemetry/test/spanExporter.test.ts

+30
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,34 @@ describe('createTransactionForOtelSpan', () => {
135135
);
136136
});
137137
});
138+
139+
it('adds span link to the trace context when linked in span options', () => {
140+
const span = startInactiveSpan({ name: 'parent1' });
141+
142+
const prevTraceId = span.spanContext().traceId;
143+
const prevSpanId = span.spanContext().spanId;
144+
145+
const linkedSpan = startInactiveSpan({
146+
name: 'parent2',
147+
links: [{ context: span.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } }],
148+
});
149+
150+
span.end();
151+
linkedSpan.end();
152+
153+
const event = createTransactionForOtelSpan(linkedSpan as any);
154+
155+
expect(event.contexts?.trace).toEqual(
156+
expect.objectContaining({
157+
links: [
158+
expect.objectContaining({
159+
attributes: { 'sentry.link.type': 'previous_trace' },
160+
sampled: true,
161+
trace_id: expect.stringMatching(prevTraceId),
162+
span_id: expect.stringMatching(prevSpanId),
163+
}),
164+
],
165+
}),
166+
);
167+
});
138168
});

packages/opentelemetry/test/trace.test.ts

+114
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,44 @@ describe('trace', () => {
390390
});
391391
});
392392

393+
it('allows to pass span links in span options', () => {
394+
const rawSpan1 = startInactiveSpan({ name: 'pageload_span' });
395+
396+
// @ts-expect-error links exists on span
397+
expect(rawSpan1?.links).toEqual([]);
398+
399+
const span1JSON = spanToJSON(rawSpan1);
400+
401+
startSpan(
402+
{
403+
name: '/users/:id',
404+
links: [
405+
{
406+
context: rawSpan1.spanContext(),
407+
attributes: { 'sentry.link.type': 'previous_trace' },
408+
},
409+
],
410+
},
411+
rawSpan2 => {
412+
const span2LinkJSON = spanToJSON(rawSpan2).links?.[0];
413+
414+
expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace');
415+
416+
// @ts-expect-error links and _spanContext exist on span
417+
expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId);
418+
// @ts-expect-error links and _spanContext exist on span
419+
expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id);
420+
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);
421+
422+
// @ts-expect-error links and _spanContext exist on span
423+
expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId);
424+
// @ts-expect-error links and _spanContext exist on span
425+
expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id);
426+
expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id);
427+
},
428+
);
429+
});
430+
393431
it('allows to force a transaction with forceTransaction=true', async () => {
394432
const client = getClient()!;
395433
const transactionEvents: Event[] = [];
@@ -651,6 +689,44 @@ describe('trace', () => {
651689
});
652690
});
653691

692+
it('allows to pass span links in span options', () => {
693+
const rawSpan1 = startInactiveSpan({ name: 'pageload_span' });
694+
695+
// @ts-expect-error links exists on span
696+
expect(rawSpan1?.links).toEqual([]);
697+
698+
const rawSpan2 = startInactiveSpan({
699+
name: 'GET users/[id]',
700+
links: [
701+
{
702+
context: rawSpan1.spanContext(),
703+
attributes: { 'sentry.link.type': 'previous_trace' },
704+
},
705+
],
706+
});
707+
708+
const span1JSON = spanToJSON(rawSpan1);
709+
const span2JSON = spanToJSON(rawSpan2);
710+
const span2LinkJSON = span2JSON.links?.[0];
711+
712+
expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace');
713+
714+
// @ts-expect-error links and _spanContext exist on span
715+
expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId);
716+
// @ts-expect-error links and _spanContext exist on span
717+
expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id);
718+
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);
719+
720+
// @ts-expect-error links and _spanContext exist on span
721+
expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId);
722+
// @ts-expect-error links and _spanContext exist on span
723+
expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id);
724+
expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id);
725+
726+
// sampling decision is inherited
727+
expect(span2LinkJSON?.sampled).toBe(Boolean(spanToJSON(rawSpan1).data['sentry.sample_rate']));
728+
});
729+
654730
it('allows to force a transaction with forceTransaction=true', async () => {
655731
const client = getClient()!;
656732
const transactionEvents: Event[] = [];
@@ -974,6 +1050,44 @@ describe('trace', () => {
9741050
});
9751051
});
9761052

1053+
it('allows to pass span links in span options', () => {
1054+
const rawSpan1 = startInactiveSpan({ name: 'pageload_span' });
1055+
1056+
// @ts-expect-error links exists on span
1057+
expect(rawSpan1?.links).toEqual([]);
1058+
1059+
const span1JSON = spanToJSON(rawSpan1);
1060+
1061+
startSpanManual(
1062+
{
1063+
name: '/users/:id',
1064+
links: [
1065+
{
1066+
context: rawSpan1.spanContext(),
1067+
attributes: { 'sentry.link.type': 'previous_trace' },
1068+
},
1069+
],
1070+
},
1071+
rawSpan2 => {
1072+
const span2LinkJSON = spanToJSON(rawSpan2).links?.[0];
1073+
1074+
expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace');
1075+
1076+
// @ts-expect-error links and _spanContext exist on span
1077+
expect(rawSpan2?.links?.[0].context.traceId).toEqual(rawSpan1._spanContext.traceId);
1078+
// @ts-expect-error links and _spanContext exist on span
1079+
expect(rawSpan2?.links?.[0].context.traceId).toEqual(span1JSON.trace_id);
1080+
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);
1081+
1082+
// @ts-expect-error links and _spanContext exist on span
1083+
expect(rawSpan2?.links?.[0].context.spanId).toEqual(rawSpan1?._spanContext.spanId);
1084+
// @ts-expect-error links and _spanContext exist on span
1085+
expect(rawSpan2?.links?.[0].context.spanId).toEqual(span1JSON.span_id);
1086+
expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id);
1087+
},
1088+
);
1089+
});
1090+
9771091
it('allows to force a transaction with forceTransaction=true', async () => {
9781092
const client = getClient()!;
9791093
const transactionEvents: Event[] = [];

0 commit comments

Comments
 (0)