Skip to content

feat(browser): Add previous_trace span links #15569

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

Merged
merged 15 commits into from
Mar 21, 2025
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration'),
gzip: true,
limit: '75.5 KB',
limit: '76 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay) - with treeshaking flags',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
const btn1 = document.getElementById('btn1');
const btn2 = document.getElementById('btn2');

btn1.addEventListener('click', () => {
Sentry.startNewTrace(() => {
Sentry.startSpan({name: 'custom root span 1', op: 'custom'}, () => {});
});
});


btn2.addEventListener('click', () => {
Sentry.startNewTrace(() => {
Sentry.startSpan({name: 'custom root span 2', op: 'custom'}, () => {});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn1">
<button id="btn2">
</button>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';

import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';

sentryTest('manually started custom traces are linked correctly in the chain', async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
await page.goto(url);
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);
return pageloadRequest.contexts?.trace;
});

const customTrace1Context = await sentryTest.step('Custom trace', async () => {
const customTrace1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'custom');
await page.locator('#btn1').click();
const customTrace1Event = envelopeRequestParser(await customTrace1RequestPromise);

const customTraceCtx = customTrace1Event.contexts?.trace;

expect(customTraceCtx?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);
expect(customTraceCtx?.links).toEqual([
{
trace_id: pageloadTraceContext?.trace_id,
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
},
},
]);

return customTraceCtx;
});

await sentryTest.step('Navigation', async () => {
const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
await page.goto(`${url}#foo`);
const navigationEvent = envelopeRequestParser(await navigation1RequestPromise);
const navTraceContext = navigationEvent.contexts?.trace;

expect(navTraceContext?.trace_id).not.toEqual(customTrace1Context?.trace_id);
expect(navTraceContext?.trace_id).not.toEqual(pageloadTraceContext?.trace_id);

expect(navTraceContext?.links).toEqual([
{
trace_id: customTrace1Context?.trace_id,
span_id: customTrace1Context?.span_id,
sampled: true,
attributes: {
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
},
},
]);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';

import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';

sentryTest("navigation spans link back to previous trace's root span", async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
await page.goto(url);
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);
return pageloadRequest.contexts?.trace;
});

const navigation1TraceContext = await sentryTest.step('First navigation', async () => {
const navigation1RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
await page.goto(`${url}#foo`);
const navigation1Request = envelopeRequestParser(await navigation1RequestPromise);
return navigation1Request.contexts?.trace;
});

const navigation2TraceContext = await sentryTest.step('Second navigation', async () => {
const navigation2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
await page.goto(`${url}#bar`);
const navigation2Request = envelopeRequestParser(await navigation2RequestPromise);
return navigation2Request.contexts?.trace;
});

const pageloadTraceId = pageloadTraceContext?.trace_id;
const navigation1TraceId = navigation1TraceContext?.trace_id;
const navigation2TraceId = navigation2TraceContext?.trace_id;

expect(pageloadTraceContext?.links).toBeUndefined();

expect(navigation1TraceContext?.links).toEqual([
{
trace_id: pageloadTraceId,
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
},
},
]);

expect(navigation2TraceContext?.links).toEqual([
{
trace_id: navigation1TraceId,
span_id: navigation1TraceContext?.span_id,
sampled: true,
attributes: {
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
},
},
]);

expect(pageloadTraceId).not.toEqual(navigation1TraceId);
expect(navigation1TraceId).not.toEqual(navigation2TraceId);
expect(pageloadTraceId).not.toEqual(navigation2TraceId);
});

sentryTest("doesn't link between hard page reloads by default", async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

await sentryTest.step('First pageload', async () => {
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
await page.goto(url);
const pageload1Event = envelopeRequestParser(await pageloadRequestPromise);

expect(pageload1Event.contexts?.trace).toBeDefined();
expect(pageload1Event.contexts?.trace?.links).toBeUndefined();
});

await sentryTest.step('Second pageload', async () => {
const pageload2RequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
await page.reload();
const pageload2Event = envelopeRequestParser(await pageload2RequestPromise);

expect(pageload2Event.contexts?.trace).toBeDefined();
expect(pageload2Event.contexts?.trace?.links).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';

import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';

/*
This is quite peculiar behavior but it's a result of the route-based trace lifetime.
Once we shortened trace lifetime, this whole scenario will change as the interaction
spans will be their own trace. So most likely, we can replace this test with a new one
that covers the new default behavior.
*/
sentryTest(
'only the first root spans in the trace link back to the previous trace',
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
await page.goto(url);

const pageloadEvent = envelopeRequestParser(await pageloadRequestPromise);
const traceContext = pageloadEvent.contexts?.trace;

expect(traceContext).toBeDefined();
expect(traceContext?.links).toBeUndefined();

return traceContext;
});

await sentryTest.step('Click Before navigation', async () => {
const interactionRequestPromise = waitForTransactionRequest(page, evt => {
return evt.contexts?.trace?.op === 'ui.action.click';
});
await page.click('#btn');

const interactionEvent = envelopeRequestParser(await interactionRequestPromise);
const interactionTraceContext = interactionEvent.contexts?.trace;

// sanity check: route-based trace lifetime means the trace_id should be the same
expect(interactionTraceContext?.trace_id).toBe(pageloadTraceContext?.trace_id);

// no links yet as previous root span belonged to same trace
expect(interactionTraceContext?.links).toBeUndefined();
});

const navigationTraceContext = await sentryTest.step('Navigation', async () => {
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
await page.goto(`${url}#foo`);
const navigationEvent = envelopeRequestParser(await navigationRequestPromise);

const traceContext = navigationEvent.contexts?.trace;

expect(traceContext?.op).toBe('navigation');
expect(traceContext?.links).toEqual([
{
trace_id: pageloadTraceContext?.trace_id,
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
},
},
]);

expect(traceContext?.trace_id).not.toEqual(traceContext?.links![0].trace_id);
return traceContext;
});

await sentryTest.step('Click After navigation', async () => {
const interactionRequestPromise = waitForTransactionRequest(page, evt => {
return evt.contexts?.trace?.op === 'ui.action.click';
});
await page.click('#btn');
const interactionEvent = envelopeRequestParser(await interactionRequestPromise);

const interactionTraceContext = interactionEvent.contexts?.trace;

// sanity check: route-based trace lifetime means the trace_id should be the same
expect(interactionTraceContext?.trace_id).toBe(navigationTraceContext?.trace_id);

// since this is the second root span in the trace, it doesn't link back to the previous trace
expect(interactionTraceContext?.links).toBeUndefined();
});
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="sentry-trace" content="12345678901234567890123456789012-1234567890123456-1" />
<meta name="baggage"
content="sentry-trace_id=12345678901234567890123456789012,sentry-sample_rate=0.2,sentry-sampled=true,sentry-transaction=my-transaction,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42"/>
</head>
</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { expect } from '@playwright/test';
import { SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE } from '@sentry/core';

import { sentryTest } from '../../../../../utils/fixtures';
import { envelopeRequestParser, shouldSkipTracingTest, waitForTransactionRequest } from '../../../../../utils/helpers';

sentryTest(
"links back to previous trace's local root span if continued from meta tags",
async ({ getLocalTestUrl, page }) => {
if (shouldSkipTracingTest()) {
sentryTest.skip();
}

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

const metaTagTraceId = '12345678901234567890123456789012';

const pageloadTraceContext = await sentryTest.step('Initial pageload', async () => {
const pageloadRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'pageload');
await page.goto(url);
const pageloadRequest = envelopeRequestParser(await pageloadRequestPromise);

const traceContext = pageloadRequest.contexts?.trace;

// sanity check
expect(traceContext?.trace_id).toBe(metaTagTraceId);

expect(traceContext?.links).toBeUndefined();

return traceContext;
});

const navigationTraceContext = await sentryTest.step('Navigation', async () => {
const navigationRequestPromise = waitForTransactionRequest(page, evt => evt.contexts?.trace?.op === 'navigation');
await page.goto(`${url}#foo`);
const navigationRequest = envelopeRequestParser(await navigationRequestPromise);
return navigationRequest.contexts?.trace;
});

const navigationTraceId = navigationTraceContext?.trace_id;

expect(navigationTraceContext?.links).toEqual([
{
trace_id: metaTagTraceId,
span_id: pageloadTraceContext?.span_id,
sampled: true,
attributes: {
[SEMANTIC_LINK_ATTRIBUTE_LINK_TYPE]: 'previous_trace',
},
},
]);

expect(navigationTraceId).not.toEqual(metaTagTraceId);
},
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://[email protected]/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
Loading