Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampler: (ctx) => {
if (ctx.attributes['sentry.origin'] === 'auto.pageload.browser') {
return 0;
}
return 1;
}
});
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); feat(browser): Add `previous_trace` span links by Lms24 · Pull Request #15569 · getsentry/sentry-javascript · GitHub
Skip to content
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
integrations: [Sentry.browserTracingIntegration()],
tracesSampleRate: 1,
debug: true,
});
Original file line numberDiff line numberDiff 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',
tracesSampleRate: 1,
integrations: [Sentry.browserTracingIntegration({_experiments: {enableInteractions: true}})],
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
</head>
<button id="btn">
</button>
</html>
Original file line numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
import * as Sentry from '@sentry/browser';

window.Sentry = Sentry;

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