Skip to content

Commit 3590824

Browse files
committed
feat(browser): Trace continuation from server-timing headers
1 parent ba7f90a commit 3590824

File tree

10 files changed

+401
-4
lines changed

10 files changed

+401
-4
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ module.exports = [
243243
import: createImport('init'),
244244
ignore: ['$app/stores'],
245245
gzip: true,
246-
limit: '42 KB',
246+
limit: '42.5 KB',
247247
},
248248
// Node-Core SDK (ESM)
249249
{
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
const errorBtn = document.getElementById('errorBtn');
2+
errorBtn.addEventListener('click', () => {
3+
throw new Error(`Sentry Test Error ${Math.random()}`);
4+
});
5+
6+
const fetchBtn = document.getElementById('fetchBtn');
7+
fetchBtn.addEventListener('click', async () => {
8+
await fetch('http://sentry-test-site.example');
9+
});
10+
11+
const xhrBtn = document.getElementById('xhrBtn');
12+
xhrBtn.addEventListener('click', () => {
13+
const xhr = new XMLHttpRequest();
14+
xhr.open('GET', 'http://sentry-test-site.example');
15+
xhr.send();
16+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="errorBtn">Throw Error</button>
8+
<button id="fetchBtn">Fetch Request</button>
9+
<button id="xhrBtn">XHR Request</button>
10+
</body>
11+
</html>
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import type { EventAndTraceHeader } from '../../../../utils/helpers';
4+
import {
5+
eventAndTraceHeaderRequestParser,
6+
getFirstSentryEnvelopeRequest,
7+
shouldSkipTracingTest,
8+
} from '../../../../utils/helpers';
9+
10+
const META_TAG_TRACE_ID = '12345678901234567890123456789012';
11+
const META_TAG_PARENT_SPAN_ID = '1234567890123456';
12+
const META_TAG_BAGGAGE =
13+
'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';
14+
15+
sentryTest(
16+
'create a new trace for a navigation after the server timing headers',
17+
async ({ getLocalTestUrl, page, enableConsole }) => {
18+
if (shouldSkipTracingTest()) {
19+
sentryTest.skip();
20+
}
21+
22+
enableConsole();
23+
24+
const url = await getLocalTestUrl({
25+
testDir: __dirname,
26+
responseHeaders: {
27+
'Server-Timing': `sentry-trace;desc=${META_TAG_TRACE_ID}-${META_TAG_PARENT_SPAN_ID}-1, baggage;desc="${META_TAG_BAGGAGE}"`,
28+
},
29+
});
30+
31+
const [pageloadEvent, pageloadTraceHeader] = await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
32+
page,
33+
url,
34+
eventAndTraceHeaderRequestParser,
35+
);
36+
const [navigationEvent, navigationTraceHeader] = await getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
37+
page,
38+
`${url}#foo`,
39+
eventAndTraceHeaderRequestParser,
40+
);
41+
42+
const pageloadTraceContext = pageloadEvent.contexts?.trace;
43+
const navigationTraceContext = navigationEvent.contexts?.trace;
44+
45+
expect(pageloadEvent.type).toEqual('transaction');
46+
expect(pageloadTraceContext).toMatchObject({
47+
op: 'pageload',
48+
trace_id: META_TAG_TRACE_ID,
49+
parent_span_id: META_TAG_PARENT_SPAN_ID,
50+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
51+
});
52+
53+
expect(pageloadTraceHeader).toEqual({
54+
environment: 'prod',
55+
release: '1.0.0',
56+
sample_rate: '0.2',
57+
sampled: 'true',
58+
transaction: 'my-transaction',
59+
public_key: 'public',
60+
trace_id: META_TAG_TRACE_ID,
61+
sample_rand: '0.42',
62+
});
63+
64+
expect(navigationEvent.type).toEqual('transaction');
65+
expect(navigationTraceContext).toMatchObject({
66+
op: 'navigation',
67+
trace_id: expect.stringMatching(/^[\da-f]{32}$/),
68+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
69+
});
70+
// navigation span is head of trace, so there's no parent span:
71+
expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
72+
73+
expect(navigationTraceHeader).toEqual({
74+
environment: 'production',
75+
public_key: 'public',
76+
sample_rate: '1',
77+
sampled: 'true',
78+
trace_id: navigationTraceContext?.trace_id,
79+
sample_rand: expect.any(String),
80+
});
81+
82+
expect(pageloadTraceContext?.trace_id).not.toEqual(navigationTraceContext?.trace_id);
83+
expect(pageloadTraceHeader?.sample_rand).not.toEqual(navigationTraceHeader?.sample_rand);
84+
},
85+
);

dev-packages/browser-integration-tests/suites/tracing/trace-lifetime/pageload-meta/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ sentryTest(
6464
span_id: expect.stringMatching(/^[\da-f]{16}$/),
6565
});
6666
// navigation span is head of trace, so there's no parent span:
67-
expect(navigationTraceContext?.trace_id).not.toHaveProperty('parent_span_id');
67+
expect(navigationTraceContext).not.toHaveProperty('parent_span_id');
6868

6969
expect(navigationTraceHeader).toEqual({
7070
environment: 'production',
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
// in browser TwP means not setting tracesSampleRate but adding browserTracingIntegration,
7+
dsn: 'https://[email protected]/1337',
8+
integrations: [Sentry.browserTracingIntegration()],
9+
tracePropagationTargets: ['http://sentry-test-site.example'],
10+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="errorBtn">Throw Error</button>
8+
</body>
9+
</html>
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
import type { EventAndTraceHeader } from '../../../../utils/helpers';
4+
import {
5+
eventAndTraceHeaderRequestParser,
6+
getFirstSentryEnvelopeRequest,
7+
shouldSkipTracingTest,
8+
} from '../../../../utils/helpers';
9+
10+
const META_TAG_TRACE_ID = '12345678901234567890123456789012';
11+
const META_TAG_PARENT_SPAN_ID = '1234567890123456';
12+
const META_TAG_BAGGAGE =
13+
'sentry-trace_id=12345678901234567890123456789012,sentry-public_key=public,sentry-release=1.0.0,sentry-environment=prod,sentry-sample_rand=0.42';
14+
15+
sentryTest('error on initial page has traceId from server timing headers', async ({ getLocalTestUrl, page }) => {
16+
if (shouldSkipTracingTest()) {
17+
sentryTest.skip();
18+
}
19+
20+
const url = await getLocalTestUrl({
21+
testDir: __dirname,
22+
responseHeaders: {
23+
'Server-Timing': `sentry-trace;desc=${META_TAG_TRACE_ID}-${META_TAG_PARENT_SPAN_ID}, baggage;desc="${META_TAG_BAGGAGE}"`,
24+
},
25+
});
26+
await page.goto(url);
27+
28+
const errorEventPromise = getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
29+
page,
30+
undefined,
31+
eventAndTraceHeaderRequestParser,
32+
);
33+
34+
await page.locator('#errorBtn').click();
35+
const [errorEvent, errorTraceHeader] = await errorEventPromise;
36+
37+
expect(errorEvent.type).toEqual(undefined);
38+
expect(errorEvent.contexts?.trace).toEqual({
39+
trace_id: META_TAG_TRACE_ID,
40+
parent_span_id: META_TAG_PARENT_SPAN_ID,
41+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
42+
});
43+
44+
expect(errorTraceHeader).toEqual({
45+
environment: 'prod',
46+
public_key: 'public',
47+
release: '1.0.0',
48+
trace_id: META_TAG_TRACE_ID,
49+
sample_rand: '0.42',
50+
});
51+
});

packages/browser/src/tracing/browserTracingIntegration.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,9 @@ export const browserTracingIntegration = ((options: Partial<BrowserTracingOption
594594
}
595595
maybeEndActiveSpan();
596596

597-
const sentryTrace = traceOptions.sentryTrace || getMetaContent('sentry-trace');
598-
const baggage = traceOptions.baggage || getMetaContent('baggage');
597+
const sentryTrace =
598+
traceOptions.sentryTrace || getMetaContent('sentry-trace') || getServerTiming('sentry-trace');
599+
const baggage = traceOptions.baggage || getMetaContent('baggage') || getServerTiming('baggage');
599600

600601
const propagationContext = propagationContextFromHeaders(sentryTrace, baggage);
601602

@@ -778,6 +779,13 @@ export function getMetaContent(metaName: string): string | undefined {
778779
return metaTag?.getAttribute('content') || undefined;
779780
}
780781

782+
/** Returns the description of a server timing entry */
783+
export function getServerTiming(name: string): string | undefined {
784+
const navigation = WINDOW.performance?.getEntriesByType?.('navigation')[0] as PerformanceNavigationTiming | undefined;
785+
const entry = navigation?.serverTiming?.find(entry => entry.name === name);
786+
return entry?.description;
787+
}
788+
781789
/** Start listener for interaction transactions */
782790
function registerInteractionListener(
783791
client: Client,

0 commit comments

Comments
 (0)