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://public@dsn.ingest.sentry.io/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://public@dsn.ingest.sentry.io/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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy