Skip to content

Commit 63ea300

Browse files
authored
feat(core): Add addLink(s) to Sentry span (#15452)
part of #14991
1 parent bac7387 commit 63ea300

File tree

10 files changed

+338
-5
lines changed

10 files changed

+338
-5
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [],
8+
tracesSampleRate: 1,
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// REGULAR ---
2+
const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' });
3+
rootSpan1.end();
4+
5+
Sentry.startSpan({ name: 'rootSpan2' }, rootSpan2 => {
6+
rootSpan2.addLink({
7+
context: rootSpan1.spanContext(),
8+
attributes: { 'sentry.link.type': 'previous_trace' },
9+
});
10+
});
11+
12+
// NESTED ---
13+
Sentry.startSpan({ name: 'rootSpan3' }, async rootSpan3 => {
14+
Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => {
15+
childSpan1.addLink({
16+
context: rootSpan1.spanContext(),
17+
attributes: { 'sentry.link.type': 'previous_trace' },
18+
});
19+
20+
childSpan1.end();
21+
});
22+
23+
Sentry.startSpan({ name: 'childSpan3.2' }, async childSpan2 => {
24+
childSpan2.addLink({ context: rootSpan3.spanContext() });
25+
26+
childSpan2.end();
27+
});
28+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { expect } from '@playwright/test';
2+
import type { SpanJSON, TransactionEvent } from '@sentry/core';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';
5+
6+
sentryTest('should link spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipTracingTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1');
12+
const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2');
13+
14+
const url = await getLocalTestUrl({ testDir: __dirname });
15+
await page.goto(url);
16+
17+
const rootSpan1 = envelopeRequestParser<TransactionEvent>(await rootSpan1Promise);
18+
const rootSpan2 = envelopeRequestParser<TransactionEvent>(await rootSpan2Promise);
19+
20+
const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string;
21+
const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;
22+
23+
expect(rootSpan1.transaction).toBe('rootSpan1');
24+
expect(rootSpan1.spans).toEqual([]);
25+
26+
expect(rootSpan2.transaction).toBe('rootSpan2');
27+
expect(rootSpan2.spans).toEqual([]);
28+
29+
expect(rootSpan2.contexts?.trace?.links?.length).toBe(1);
30+
expect(rootSpan2.contexts?.trace?.links?.[0]).toMatchObject({
31+
attributes: { 'sentry.link.type': 'previous_trace' },
32+
sampled: true,
33+
span_id: rootSpan1_spanId,
34+
trace_id: rootSpan1_traceId,
35+
});
36+
});
37+
38+
sentryTest('should link spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => {
39+
if (shouldSkipTracingTest()) {
40+
sentryTest.skip();
41+
}
42+
43+
const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1');
44+
const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3');
45+
46+
const url = await getLocalTestUrl({ testDir: __dirname });
47+
await page.goto(url);
48+
49+
const rootSpan1 = envelopeRequestParser<TransactionEvent>(await rootSpan1Promise);
50+
const rootSpan3 = envelopeRequestParser<TransactionEvent>(await rootSpan3Promise);
51+
52+
const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string;
53+
const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;
54+
55+
const [childSpan_3_1, childSpan_3_2] = rootSpan3.spans as [SpanJSON, SpanJSON];
56+
const rootSpan3_traceId = rootSpan3.contexts?.trace?.trace_id as string;
57+
const rootSpan3_spanId = rootSpan3.contexts?.trace?.span_id as string;
58+
59+
expect(rootSpan3.transaction).toBe('rootSpan3');
60+
61+
expect(childSpan_3_1.description).toBe('childSpan3.1');
62+
expect(childSpan_3_1.links?.length).toBe(1);
63+
expect(childSpan_3_1.links?.[0]).toMatchObject({
64+
attributes: { 'sentry.link.type': 'previous_trace' },
65+
sampled: true,
66+
span_id: rootSpan1_spanId,
67+
trace_id: rootSpan1_traceId,
68+
});
69+
70+
expect(childSpan_3_2.description).toBe('childSpan3.2');
71+
expect(childSpan_3_2.links?.[0]).toMatchObject({
72+
sampled: true,
73+
span_id: rootSpan3_spanId,
74+
trace_id: rootSpan3_traceId,
75+
});
76+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://[email protected]/1337',
7+
integrations: [],
8+
tracesSampleRate: 1,
9+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// REGULAR ---
2+
const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' });
3+
rootSpan1.end();
4+
5+
const rootSpan2 = Sentry.startInactiveSpan({ name: 'rootSpan2' });
6+
rootSpan2.end();
7+
8+
Sentry.startSpan({ name: 'rootSpan3' }, rootSpan3 => {
9+
rootSpan3.addLinks([
10+
{ context: rootSpan1.spanContext() },
11+
{
12+
context: rootSpan2.spanContext(),
13+
attributes: { 'sentry.link.type': 'previous_trace' },
14+
},
15+
]);
16+
});
17+
18+
// NESTED ---
19+
Sentry.startSpan({ name: 'rootSpan4' }, async rootSpan4 => {
20+
Sentry.startSpan({ name: 'childSpan4.1' }, async childSpan1 => {
21+
Sentry.startSpan({ name: 'childSpan4.2' }, async childSpan2 => {
22+
childSpan2.addLinks([
23+
{ context: rootSpan4.spanContext() },
24+
{
25+
context: rootSpan2.spanContext(),
26+
attributes: { 'sentry.link.type': 'previous_trace' },
27+
},
28+
]);
29+
30+
childSpan2.end();
31+
});
32+
33+
childSpan1.end();
34+
});
35+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { expect } from '@playwright/test';
2+
import type { SpanJSON, TransactionEvent } from '@sentry/core';
3+
import { sentryTest } from '../../../utils/fixtures';
4+
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';
5+
6+
sentryTest('should link spans with addLinks() in trace context', async ({ getLocalTestUrl, page }) => {
7+
if (shouldSkipTracingTest()) {
8+
sentryTest.skip();
9+
}
10+
11+
const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1');
12+
const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2');
13+
const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3');
14+
15+
const url = await getLocalTestUrl({ testDir: __dirname });
16+
await page.goto(url);
17+
18+
const rootSpan1 = envelopeRequestParser<TransactionEvent>(await rootSpan1Promise);
19+
const rootSpan2 = envelopeRequestParser<TransactionEvent>(await rootSpan2Promise);
20+
const rootSpan3 = envelopeRequestParser<TransactionEvent>(await rootSpan3Promise);
21+
22+
const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string;
23+
const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;
24+
25+
expect(rootSpan1.transaction).toBe('rootSpan1');
26+
expect(rootSpan1.spans).toEqual([]);
27+
28+
const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string;
29+
const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string;
30+
31+
expect(rootSpan2.transaction).toBe('rootSpan2');
32+
expect(rootSpan2.spans).toEqual([]);
33+
34+
expect(rootSpan3.transaction).toBe('rootSpan3');
35+
expect(rootSpan3.spans).toEqual([]);
36+
expect(rootSpan3.contexts?.trace?.links?.length).toBe(2);
37+
expect(rootSpan3.contexts?.trace?.links).toEqual([
38+
{
39+
sampled: true,
40+
span_id: rootSpan1_spanId,
41+
trace_id: rootSpan1_traceId,
42+
},
43+
{
44+
attributes: { 'sentry.link.type': 'previous_trace' },
45+
sampled: true,
46+
span_id: rootSpan2_spanId,
47+
trace_id: rootSpan2_traceId,
48+
},
49+
]);
50+
});
51+
52+
sentryTest('should link spans with addLinks() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => {
53+
if (shouldSkipTracingTest()) {
54+
sentryTest.skip();
55+
}
56+
57+
const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2');
58+
const rootSpan4Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan4');
59+
60+
const url = await getLocalTestUrl({ testDir: __dirname });
61+
await page.goto(url);
62+
63+
const rootSpan2 = envelopeRequestParser<TransactionEvent>(await rootSpan2Promise);
64+
const rootSpan4 = envelopeRequestParser<TransactionEvent>(await rootSpan4Promise);
65+
66+
const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string;
67+
const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string;
68+
69+
const [childSpan_4_1, childSpan_4_2] = rootSpan4.spans as [SpanJSON, SpanJSON];
70+
const rootSpan4_traceId = rootSpan4.contexts?.trace?.trace_id as string;
71+
const rootSpan4_spanId = rootSpan4.contexts?.trace?.span_id as string;
72+
73+
expect(rootSpan4.transaction).toBe('rootSpan4');
74+
75+
expect(childSpan_4_1.description).toBe('childSpan4.1');
76+
expect(childSpan_4_1.links).toBe(undefined);
77+
78+
expect(childSpan_4_2.description).toBe('childSpan4.2');
79+
expect(childSpan_4_2.links?.length).toBe(2);
80+
expect(childSpan_4_2.links).toEqual([
81+
{
82+
sampled: true,
83+
span_id: rootSpan4_spanId,
84+
trace_id: rootSpan4_traceId,
85+
},
86+
{
87+
attributes: { 'sentry.link.type': 'previous_trace' },
88+
sampled: true,
89+
span_id: rootSpan2_spanId,
90+
trace_id: rootSpan2_traceId,
91+
},
92+
]);
93+
});

packages/core/src/tracing/sentrySpan.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@ import type {
2424
TransactionEvent,
2525
TransactionSource,
2626
} from '../types-hoist';
27+
import type { SpanLink } from '../types-hoist/link';
2728
import { logger } from '../utils-hoist/logger';
2829
import { dropUndefinedKeys } from '../utils-hoist/object';
2930
import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext';
3031
import { timestampInSeconds } from '../utils-hoist/time';
3132
import {
3233
TRACE_FLAG_NONE,
3334
TRACE_FLAG_SAMPLED,
35+
convertSpanLinksForEnvelope,
3436
getRootSpan,
3537
getSpanDescendants,
3638
getStatusMessage,
@@ -55,6 +57,7 @@ export class SentrySpan implements Span {
5557
protected _sampled: boolean | undefined;
5658
protected _name?: string | undefined;
5759
protected _attributes: SpanAttributes;
60+
protected _links?: SpanLink[];
5861
/** Epoch timestamp in seconds when the span started. */
5962
protected _startTime: number;
6063
/** Epoch timestamp in seconds when the span ended. */
@@ -110,12 +113,22 @@ export class SentrySpan implements Span {
110113
}
111114

112115
/** @inheritDoc */
113-
public addLink(_link: unknown): this {
116+
public addLink(link: SpanLink): this {
117+
if (this._links) {
118+
this._links.push(link);
119+
} else {
120+
this._links = [link];
121+
}
114122
return this;
115123
}
116124

117125
/** @inheritDoc */
118-
public addLinks(_links: unknown[]): this {
126+
public addLinks(links: SpanLink[]): this {
127+
if (this._links) {
128+
this._links.push(...links);
129+
} else {
130+
this._links = links;
131+
}
119132
return this;
120133
}
121134

@@ -225,6 +238,7 @@ export class SentrySpan implements Span {
225238
measurements: timedEventsToMeasurements(this._events),
226239
is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined,
227240
segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined,
241+
links: convertSpanLinksForEnvelope(this._links),
228242
});
229243
}
230244

packages/core/src/utils/spanUtils.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ let hasShownSpanDropWarning = false;
4040
*/
4141
export function spanToTransactionTraceContext(span: Span): TraceContext {
4242
const { spanId: span_id, traceId: trace_id } = span.spanContext();
43-
const { data, op, parent_span_id, status, origin } = spanToJSON(span);
43+
const { data, op, parent_span_id, status, origin, links } = spanToJSON(span);
4444

4545
return dropUndefinedKeys({
4646
parent_span_id,
@@ -50,6 +50,7 @@ export function spanToTransactionTraceContext(span: Span): TraceContext {
5050
op,
5151
status,
5252
origin,
53+
links,
5354
});
5455
}
5556

0 commit comments

Comments
 (0)