Skip to content

Commit

Permalink
feat(core): Add addLink(s) to Sentry span (#15452)
Browse files Browse the repository at this point in the history
part of #14991
  • Loading branch information
s1gr1d authored Feb 24, 2025
1 parent bac7387 commit 63ea300
Show file tree
Hide file tree
Showing 10 changed files with 338 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// REGULAR ---
const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' });
rootSpan1.end();

Sentry.startSpan({ name: 'rootSpan2' }, rootSpan2 => {
rootSpan2.addLink({
context: rootSpan1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
});
});

// NESTED ---
Sentry.startSpan({ name: 'rootSpan3' }, async rootSpan3 => {
Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => {
childSpan1.addLink({
context: rootSpan1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
});

childSpan1.end();
});

Sentry.startSpan({ name: 'childSpan3.2' }, async childSpan2 => {
childSpan2.addLink({ context: rootSpan3.spanContext() });

childSpan2.end();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { expect } from '@playwright/test';
import type { SpanJSON, TransactionEvent } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';

sentryTest('should link spans with addLink() in trace context', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1');
const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2');

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const rootSpan1 = envelopeRequestParser<TransactionEvent>(await rootSpan1Promise);
const rootSpan2 = envelopeRequestParser<TransactionEvent>(await rootSpan2Promise);

const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string;
const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;

expect(rootSpan1.transaction).toBe('rootSpan1');
expect(rootSpan1.spans).toEqual([]);

expect(rootSpan2.transaction).toBe('rootSpan2');
expect(rootSpan2.spans).toEqual([]);

expect(rootSpan2.contexts?.trace?.links?.length).toBe(1);
expect(rootSpan2.contexts?.trace?.links?.[0]).toMatchObject({
attributes: { 'sentry.link.type': 'previous_trace' },
sampled: true,
span_id: rootSpan1_spanId,
trace_id: rootSpan1_traceId,
});
});

sentryTest('should link spans with addLink() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1');
const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3');

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const rootSpan1 = envelopeRequestParser<TransactionEvent>(await rootSpan1Promise);
const rootSpan3 = envelopeRequestParser<TransactionEvent>(await rootSpan3Promise);

const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string;
const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;

const [childSpan_3_1, childSpan_3_2] = rootSpan3.spans as [SpanJSON, SpanJSON];
const rootSpan3_traceId = rootSpan3.contexts?.trace?.trace_id as string;
const rootSpan3_spanId = rootSpan3.contexts?.trace?.span_id as string;

expect(rootSpan3.transaction).toBe('rootSpan3');

expect(childSpan_3_1.description).toBe('childSpan3.1');
expect(childSpan_3_1.links?.length).toBe(1);
expect(childSpan_3_1.links?.[0]).toMatchObject({
attributes: { 'sentry.link.type': 'previous_trace' },
sampled: true,
span_id: rootSpan1_spanId,
trace_id: rootSpan1_traceId,
});

expect(childSpan_3_2.description).toBe('childSpan3.2');
expect(childSpan_3_2.links?.[0]).toMatchObject({
sampled: true,
span_id: rootSpan3_spanId,
trace_id: rootSpan3_traceId,
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// REGULAR ---
const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' });
rootSpan1.end();

const rootSpan2 = Sentry.startInactiveSpan({ name: 'rootSpan2' });
rootSpan2.end();

Sentry.startSpan({ name: 'rootSpan3' }, rootSpan3 => {
rootSpan3.addLinks([
{ context: rootSpan1.spanContext() },
{
context: rootSpan2.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
},
]);
});

// NESTED ---
Sentry.startSpan({ name: 'rootSpan4' }, async rootSpan4 => {
Sentry.startSpan({ name: 'childSpan4.1' }, async childSpan1 => {
Sentry.startSpan({ name: 'childSpan4.2' }, async childSpan2 => {
childSpan2.addLinks([
{ context: rootSpan4.spanContext() },
{
context: rootSpan2.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
},
]);

childSpan2.end();
});

childSpan1.end();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { expect } from '@playwright/test';
import type { SpanJSON, TransactionEvent } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';

sentryTest('should link spans with addLinks() in trace context', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const rootSpan1Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan1');
const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2');
const rootSpan3Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan3');

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const rootSpan1 = envelopeRequestParser<TransactionEvent>(await rootSpan1Promise);
const rootSpan2 = envelopeRequestParser<TransactionEvent>(await rootSpan2Promise);
const rootSpan3 = envelopeRequestParser<TransactionEvent>(await rootSpan3Promise);

const rootSpan1_traceId = rootSpan1.contexts?.trace?.trace_id as string;
const rootSpan1_spanId = rootSpan1.contexts?.trace?.span_id as string;

expect(rootSpan1.transaction).toBe('rootSpan1');
expect(rootSpan1.spans).toEqual([]);

const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string;
const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string;

expect(rootSpan2.transaction).toBe('rootSpan2');
expect(rootSpan2.spans).toEqual([]);

expect(rootSpan3.transaction).toBe('rootSpan3');
expect(rootSpan3.spans).toEqual([]);
expect(rootSpan3.contexts?.trace?.links?.length).toBe(2);
expect(rootSpan3.contexts?.trace?.links).toEqual([
{
sampled: true,
span_id: rootSpan1_spanId,
trace_id: rootSpan1_traceId,
},
{
attributes: { 'sentry.link.type': 'previous_trace' },
sampled: true,
span_id: rootSpan2_spanId,
trace_id: rootSpan2_traceId,
},
]);
});

sentryTest('should link spans with addLinks() in nested startSpan() calls', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

const rootSpan2Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan2');
const rootSpan4Promise = waitForTransactionRequest(page, event => event.transaction === 'rootSpan4');

const url = await getLocalTestUrl({ testDir: __dirname });
await page.goto(url);

const rootSpan2 = envelopeRequestParser<TransactionEvent>(await rootSpan2Promise);
const rootSpan4 = envelopeRequestParser<TransactionEvent>(await rootSpan4Promise);

const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string;
const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string;

const [childSpan_4_1, childSpan_4_2] = rootSpan4.spans as [SpanJSON, SpanJSON];
const rootSpan4_traceId = rootSpan4.contexts?.trace?.trace_id as string;
const rootSpan4_spanId = rootSpan4.contexts?.trace?.span_id as string;

expect(rootSpan4.transaction).toBe('rootSpan4');

expect(childSpan_4_1.description).toBe('childSpan4.1');
expect(childSpan_4_1.links).toBe(undefined);

expect(childSpan_4_2.description).toBe('childSpan4.2');
expect(childSpan_4_2.links?.length).toBe(2);
expect(childSpan_4_2.links).toEqual([
{
sampled: true,
span_id: rootSpan4_spanId,
trace_id: rootSpan4_traceId,
},
{
attributes: { 'sentry.link.type': 'previous_trace' },
sampled: true,
span_id: rootSpan2_spanId,
trace_id: rootSpan2_traceId,
},
]);
});
18 changes: 16 additions & 2 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,15 @@ import type {
TransactionEvent,
TransactionSource,
} from '../types-hoist';
import type { SpanLink } from '../types-hoist/link';
import { logger } from '../utils-hoist/logger';
import { dropUndefinedKeys } from '../utils-hoist/object';
import { generateSpanId, generateTraceId } from '../utils-hoist/propagationContext';
import { timestampInSeconds } from '../utils-hoist/time';
import {
TRACE_FLAG_NONE,
TRACE_FLAG_SAMPLED,
convertSpanLinksForEnvelope,
getRootSpan,
getSpanDescendants,
getStatusMessage,
Expand All @@ -55,6 +57,7 @@ export class SentrySpan implements Span {
protected _sampled: boolean | undefined;
protected _name?: string | undefined;
protected _attributes: SpanAttributes;
protected _links?: SpanLink[];
/** Epoch timestamp in seconds when the span started. */
protected _startTime: number;
/** Epoch timestamp in seconds when the span ended. */
Expand Down Expand Up @@ -110,12 +113,22 @@ export class SentrySpan implements Span {
}

/** @inheritDoc */
public addLink(_link: unknown): this {
public addLink(link: SpanLink): this {
if (this._links) {
this._links.push(link);
} else {
this._links = [link];
}
return this;
}

/** @inheritDoc */
public addLinks(_links: unknown[]): this {
public addLinks(links: SpanLink[]): this {
if (this._links) {
this._links.push(...links);
} else {
this._links = links;
}
return this;
}

Expand Down Expand Up @@ -225,6 +238,7 @@ export class SentrySpan implements Span {
measurements: timedEventsToMeasurements(this._events),
is_segment: (this._isStandaloneSpan && getRootSpan(this) === this) || undefined,
segment_id: this._isStandaloneSpan ? getRootSpan(this).spanContext().spanId : undefined,
links: convertSpanLinksForEnvelope(this._links),
});
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/utils/spanUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ let hasShownSpanDropWarning = false;
*/
export function spanToTransactionTraceContext(span: Span): TraceContext {
const { spanId: span_id, traceId: trace_id } = span.spanContext();
const { data, op, parent_span_id, status, origin } = spanToJSON(span);
const { data, op, parent_span_id, status, origin, links } = spanToJSON(span);

return dropUndefinedKeys({
parent_span_id,
Expand All @@ -50,6 +50,7 @@ export function spanToTransactionTraceContext(span: Span): TraceContext {
op,
status,
origin,
links,
});
}

Expand Down
Loading

0 comments on commit 63ea300

Please sign in to comment.