Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(core): Add links to span options #15453

Merged
merged 4 commits into from
Feb 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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://public@dsn.ingest.sentry.io/1337',
integrations: [],
tracesSampleRate: 1,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const rootSpan1 = Sentry.startInactiveSpan({ name: 'rootSpan1' });
rootSpan1.end();

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

Sentry.startSpan(
{
name: 'rootSpan3',
links: [
{ context: rootSpan1.spanContext() },
{ context: rootSpan2.spanContext(), attributes: { 'sentry.link.type': 'previous_trace' } },
],
},
async () => {
Sentry.startSpan({ name: 'childSpan3.1' }, async childSpan1 => {
childSpan1.end();
});
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { expect } from '@playwright/test';
import type { TransactionEvent } from '@sentry/core';
import { sentryTest } from '../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../utils/helpers';

sentryTest('should link spans by adding "links" to span options', 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;
const rootSpan2_traceId = rootSpan2.contexts?.trace?.trace_id as string;
const rootSpan2_spanId = rootSpan2.contexts?.trace?.span_id as string;

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

expect(rootSpan3.transaction).toBe('rootSpan3');
expect(rootSpan3.spans?.length).toBe(1);
expect(rootSpan3.spans?.[0].description).toBe('childSpan3.1');

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,
},
]);
});
1 change: 1 addition & 0 deletions packages/core/src/tracing/sentrySpan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class SentrySpan implements Span {
this._traceId = spanContext.traceId || generateTraceId();
this._spanId = spanContext.spanId || generateSpanId();
this._startTime = spanContext.startTimestamp || timestampInSeconds();
this._links = spanContext.links;

this._attributes = {};
this.setAttributes({
Expand Down
38 changes: 38 additions & 0 deletions packages/core/test/lib/tracing/trace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,44 @@ describe('startInactiveSpan', () => {
});
});

it('allows to pass span links in span options', () => {
const rawSpan1 = startInactiveSpan({ name: 'pageload_span' });

// @ts-expect-error _links exists on span
expect(rawSpan1?._links).toEqual(undefined);

const rawSpan2 = startInactiveSpan({
name: 'GET users/[id]',
links: [
{
context: rawSpan1.spanContext(),
attributes: { 'sentry.link.type': 'previous_trace' },
},
],
});

const span1JSON = spanToJSON(rawSpan1);
const span2JSON = spanToJSON(rawSpan2);
const span2LinkJSON = span2JSON.links?.[0];

expect(span2LinkJSON?.attributes?.['sentry.link.type']).toBe('previous_trace');

// @ts-expect-error _links and _traceId exist on SentrySpan
expect(rawSpan2?._links?.[0].context.traceId).toEqual(rawSpan1._traceId);
// @ts-expect-error _links and _traceId exist on SentrySpan
expect(rawSpan2?._links?.[0].context.traceId).toEqual(span1JSON.trace_id);
expect(span2LinkJSON?.trace_id).toBe(span1JSON.trace_id);

// @ts-expect-error _links and _traceId exist on SentrySpan
expect(rawSpan2?._links?.[0].context.spanId).toEqual(rawSpan1?._spanId);
// @ts-expect-error _links and _traceId exist on SentrySpan
expect(rawSpan2?._links?.[0].context.spanId).toEqual(span1JSON.span_id);
expect(span2LinkJSON?.span_id).toBe(span1JSON.span_id);

// sampling decision is inherited
expect(span2LinkJSON?.sampled).toBe(Boolean(spanToJSON(rawSpan1).data['sentry.sample_rate']));
});

it('allows to force a transaction with forceTransaction=true', async () => {
const options = getDefaultTestClientOptions({ tracesSampleRate: 1.0 });
client = new TestClient(options);
Expand Down
Loading