Skip to content

Commit 00d7f3e

Browse files
committed
add trace lifetime tests
1 parent 3f9ee72 commit 00d7f3e

File tree

8 files changed

+657
-0
lines changed

8 files changed

+657
-0
lines changed
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as Sentry from '@sentry/browser';
2+
// Import this separately so that generatePlugin can handle it for CDN scenarios
3+
import { feedbackIntegration } from '@sentry/browser';
4+
5+
window.Sentry = Sentry;
6+
7+
Sentry.init({
8+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
9+
integrations: [Sentry.browserTracingIntegration(), feedbackIntegration(), Sentry.spanStreamingIntegration()],
10+
tracePropagationTargets: ['http://sentry-test-site.example'],
11+
tracesSampleRate: 1,
12+
});
Lines changed: 317 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,317 @@
1+
import { expect } from '@playwright/test';
2+
import type { Event } from '@sentry/core';
3+
import { sentryTest } from '../../../../utils/fixtures';
4+
import type { EventAndTraceHeader } from '../../../../utils/helpers';
5+
import {
6+
eventAndTraceHeaderRequestParser,
7+
getFirstSentryEnvelopeRequest,
8+
shouldSkipFeedbackTest,
9+
shouldSkipTracingTest,
10+
} from '../../../../utils/helpers';
11+
import { getSpanOp, waitForStreamedSpan, waitForStreamedSpanEnvelope } from '../../../../utils/spanUtils';
12+
13+
sentryTest('creates a new trace and sample_rand on each navigation', async ({ getLocalTestUrl, page }) => {
14+
sentryTest.skip(shouldSkipTracingTest());
15+
16+
const url = await getLocalTestUrl({ testDir: __dirname });
17+
18+
// Wait for and skip the initial pageload span
19+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
20+
await page.goto(url);
21+
await pageloadSpanPromise;
22+
23+
const navigation1SpanEnvelopePromise = waitForStreamedSpanEnvelope(
24+
page,
25+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'),
26+
);
27+
await page.goto(`${url}#foo`);
28+
const navigation1SpanEnvelope = await navigation1SpanEnvelopePromise;
29+
30+
const navigation2SpanEnvelopePromise = waitForStreamedSpanEnvelope(
31+
page,
32+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'),
33+
);
34+
await page.goto(`${url}#bar`);
35+
const navigation2SpanEnvelope = await navigation2SpanEnvelopePromise;
36+
37+
const navigation1TraceId = navigation1SpanEnvelope[0].trace?.trace_id;
38+
const navigation1SampleRand = navigation1SpanEnvelope[0].trace?.sample_rand;
39+
const navigation2TraceId = navigation2SpanEnvelope[0].trace?.trace_id;
40+
const navigation2SampleRand = navigation2SpanEnvelope[0].trace?.sample_rand;
41+
42+
const navigation1Span = navigation1SpanEnvelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!;
43+
const navigation2Span = navigation2SpanEnvelope[1][0][1].items.find(s => getSpanOp(s) === 'navigation')!;
44+
45+
expect(getSpanOp(navigation1Span)).toEqual('navigation');
46+
expect(navigation1TraceId).toMatch(/^[\da-f]{32}$/);
47+
expect(navigation1Span.span_id).toMatch(/^[\da-f]{16}$/);
48+
expect(navigation1Span.parent_span_id).toBeUndefined();
49+
50+
expect(navigation1SpanEnvelope[0].trace).toEqual({
51+
environment: 'production',
52+
public_key: 'public',
53+
sample_rate: '1',
54+
sampled: 'true',
55+
trace_id: navigation1TraceId,
56+
sample_rand: expect.any(String),
57+
});
58+
59+
expect(getSpanOp(navigation2Span)).toEqual('navigation');
60+
expect(navigation2TraceId).toMatch(/^[\da-f]{32}$/);
61+
expect(navigation2Span.span_id).toMatch(/^[\da-f]{16}$/);
62+
expect(navigation2Span.parent_span_id).toBeUndefined();
63+
64+
expect(navigation2SpanEnvelope[0].trace).toEqual({
65+
environment: 'production',
66+
public_key: 'public',
67+
sample_rate: '1',
68+
sampled: 'true',
69+
trace_id: navigation2TraceId,
70+
sample_rand: expect.any(String),
71+
});
72+
73+
expect(navigation1TraceId).not.toEqual(navigation2TraceId);
74+
expect(navigation1SampleRand).not.toEqual(navigation2SampleRand);
75+
});
76+
77+
sentryTest('error after navigation has navigation traceId', async ({ getLocalTestUrl, page }) => {
78+
sentryTest.skip(shouldSkipTracingTest());
79+
80+
const url = await getLocalTestUrl({ testDir: __dirname });
81+
82+
// ensure pageload span is finished
83+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
84+
await page.goto(url);
85+
await pageloadSpanPromise;
86+
87+
const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation');
88+
const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope(
89+
page,
90+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'),
91+
);
92+
await page.goto(`${url}#foo`);
93+
const [navigationSpan, navigationSpanEnvelope] = await Promise.all([
94+
navigationSpanPromise,
95+
navigationSpanEnvelopePromise,
96+
]);
97+
98+
const navigationTraceId = navigationSpan.trace_id;
99+
100+
expect(getSpanOp(navigationSpan)).toEqual('navigation');
101+
expect(navigationTraceId).toMatch(/^[\da-f]{32}$/);
102+
expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/);
103+
expect(navigationSpan.parent_span_id).toBeUndefined();
104+
105+
expect(navigationSpanEnvelope[0].trace).toEqual({
106+
environment: 'production',
107+
public_key: 'public',
108+
sample_rate: '1',
109+
sampled: 'true',
110+
trace_id: navigationTraceId,
111+
sample_rand: expect.any(String),
112+
});
113+
114+
const errorEventPromise = getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
115+
page,
116+
undefined,
117+
eventAndTraceHeaderRequestParser,
118+
);
119+
await page.locator('#errorBtn').click();
120+
const [errorEvent, errorTraceHeader] = await errorEventPromise;
121+
122+
expect(errorEvent.type).toEqual(undefined);
123+
124+
const errorTraceContext = errorEvent.contexts?.trace;
125+
expect(errorTraceContext).toEqual({
126+
trace_id: navigationTraceId,
127+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
128+
});
129+
expect(errorTraceHeader).toEqual({
130+
environment: 'production',
131+
public_key: 'public',
132+
sample_rate: '1',
133+
sampled: 'true',
134+
trace_id: navigationTraceId,
135+
sample_rand: expect.any(String),
136+
});
137+
});
138+
139+
sentryTest('error during navigation has new navigation traceId', async ({ getLocalTestUrl, page }) => {
140+
sentryTest.skip(shouldSkipTracingTest());
141+
142+
const url = await getLocalTestUrl({ testDir: __dirname });
143+
144+
// ensure pageload span is finished
145+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
146+
await page.goto(url);
147+
await pageloadSpanPromise;
148+
149+
const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation');
150+
const errorEventPromise = getFirstSentryEnvelopeRequest<EventAndTraceHeader>(
151+
page,
152+
undefined,
153+
eventAndTraceHeaderRequestParser,
154+
);
155+
156+
await page.goto(`${url}#foo`);
157+
await page.locator('#errorBtn').click();
158+
const [navigationSpan, [errorEvent, errorTraceHeader]] = await Promise.all([
159+
navigationSpanPromise,
160+
errorEventPromise,
161+
]);
162+
163+
expect(getSpanOp(navigationSpan)).toEqual('navigation');
164+
expect(errorEvent.type).toEqual(undefined);
165+
166+
const navigationTraceId = navigationSpan.trace_id;
167+
expect(navigationTraceId).toMatch(/^[\da-f]{32}$/);
168+
expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/);
169+
expect(navigationSpan.parent_span_id).toBeUndefined();
170+
171+
const errorTraceContext = errorEvent?.contexts?.trace;
172+
expect(errorTraceContext).toEqual({
173+
trace_id: navigationTraceId,
174+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
175+
});
176+
177+
expect(errorTraceHeader).toEqual({
178+
environment: 'production',
179+
public_key: 'public',
180+
sample_rate: '1',
181+
sampled: 'true',
182+
trace_id: navigationTraceId,
183+
sample_rand: expect.any(String),
184+
});
185+
});
186+
187+
sentryTest(
188+
'outgoing fetch request during navigation has navigation traceId in headers',
189+
async ({ getLocalTestUrl, page }) => {
190+
sentryTest.skip(shouldSkipTracingTest());
191+
192+
const url = await getLocalTestUrl({ testDir: __dirname });
193+
194+
await page.route('http://sentry-test-site.example/**', route => {
195+
return route.fulfill({
196+
status: 200,
197+
contentType: 'application/json',
198+
body: JSON.stringify({}),
199+
});
200+
});
201+
202+
// ensure pageload span is finished
203+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
204+
await page.goto(url);
205+
await pageloadSpanPromise;
206+
207+
const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope(
208+
page,
209+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'),
210+
);
211+
const requestPromise = page.waitForRequest('http://sentry-test-site.example/*');
212+
await page.goto(`${url}#foo`);
213+
await page.locator('#fetchBtn').click();
214+
const [navigationSpanEnvelope, request] = await Promise.all([navigationSpanEnvelopePromise, requestPromise]);
215+
216+
const navigationTraceId = navigationSpanEnvelope[0].trace?.trace_id;
217+
const sampleRand = navigationSpanEnvelope[0].trace?.sample_rand;
218+
219+
expect(navigationTraceId).toMatch(/^[\da-f]{32}$/);
220+
221+
const headers = request.headers();
222+
223+
// sampling decision is propagated from active span sampling decision
224+
expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
225+
expect(headers['baggage']).toEqual(
226+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`,
227+
);
228+
},
229+
);
230+
231+
sentryTest(
232+
'outgoing XHR request during navigation has navigation traceId in headers',
233+
async ({ getLocalTestUrl, page }) => {
234+
sentryTest.skip(shouldSkipTracingTest());
235+
236+
const url = await getLocalTestUrl({ testDir: __dirname });
237+
238+
await page.route('http://sentry-test-site.example/**', route => {
239+
return route.fulfill({
240+
status: 200,
241+
contentType: 'application/json',
242+
body: JSON.stringify({}),
243+
});
244+
});
245+
246+
// ensure navigation span is finished
247+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
248+
await page.goto(url);
249+
await pageloadSpanPromise;
250+
251+
const navigationSpanEnvelopePromise = waitForStreamedSpanEnvelope(
252+
page,
253+
env => !!env[1][0][1].items.find(s => getSpanOp(s) === 'navigation'),
254+
);
255+
const requestPromise = page.waitForRequest('http://sentry-test-site.example/*');
256+
await page.goto(`${url}#foo`);
257+
await page.locator('#xhrBtn').click();
258+
const [navigationSpanEnvelope, request] = await Promise.all([navigationSpanEnvelopePromise, requestPromise]);
259+
260+
const navigationTraceId = navigationSpanEnvelope[0].trace?.trace_id;
261+
const sampleRand = navigationSpanEnvelope[0].trace?.sample_rand;
262+
263+
expect(navigationTraceId).toMatch(/^[\da-f]{32}$/);
264+
265+
const headers = request.headers();
266+
267+
// sampling decision is propagated from active span sampling decision
268+
expect(headers['sentry-trace']).toMatch(new RegExp(`^${navigationTraceId}-[0-9a-f]{16}-1$`));
269+
expect(headers['baggage']).toEqual(
270+
`sentry-environment=production,sentry-public_key=public,sentry-trace_id=${navigationTraceId},sentry-sampled=true,sentry-sample_rand=${sampleRand},sentry-sample_rate=1`,
271+
);
272+
},
273+
);
274+
275+
sentryTest(
276+
'user feedback event after navigation has navigation traceId in headers',
277+
async ({ getLocalTestUrl, page }) => {
278+
sentryTest.skip(shouldSkipTracingTest() || shouldSkipFeedbackTest());
279+
280+
const url = await getLocalTestUrl({ testDir: __dirname, handleLazyLoadedFeedback: true });
281+
282+
// ensure pageload span is finished
283+
const pageloadSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'pageload');
284+
await page.goto(url);
285+
await pageloadSpanPromise;
286+
287+
const navigationSpanPromise = waitForStreamedSpan(page, span => getSpanOp(span) === 'navigation');
288+
await page.goto(`${url}#foo`);
289+
const navigationSpan = await navigationSpanPromise;
290+
291+
const navigationTraceId = navigationSpan.trace_id;
292+
expect(getSpanOp(navigationSpan)).toEqual('navigation');
293+
expect(navigationTraceId).toMatch(/^[\da-f]{32}$/);
294+
expect(navigationSpan.span_id).toMatch(/^[\da-f]{16}$/);
295+
expect(navigationSpan.parent_span_id).toBeUndefined();
296+
297+
const feedbackEventPromise = getFirstSentryEnvelopeRequest<Event>(page);
298+
299+
await page.getByText('Report a Bug').click();
300+
expect(await page.locator(':visible:text-is("Report a Bug")').count()).toEqual(1);
301+
await page.locator('[name="name"]').fill('Jane Doe');
302+
await page.locator('[name="email"]').fill('janedoe@example.org');
303+
await page.locator('[name="message"]').fill('my example feedback');
304+
await page.locator('[data-sentry-feedback] .btn--primary').click();
305+
306+
const feedbackEvent = await feedbackEventPromise;
307+
308+
expect(feedbackEvent.type).toEqual('feedback');
309+
310+
const feedbackTraceContext = feedbackEvent.contexts?.trace;
311+
312+
expect(feedbackTraceContext).toMatchObject({
313+
trace_id: navigationTraceId,
314+
span_id: expect.stringMatching(/^[\da-f]{16}$/),
315+
});
316+
},
317+
);
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import * as Sentry from '@sentry/browser';
2+
// Import this separately so that generatePlugin can handle it for CDN scenarios
3+
import { feedbackIntegration } from '@sentry/browser';
4+
5+
window.Sentry = Sentry;
6+
7+
Sentry.init({
8+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
9+
integrations: [Sentry.browserTracingIntegration(), feedbackIntegration(), Sentry.spanStreamingIntegration()],
10+
tracePropagationTargets: ['http://sentry-test-site.example'],
11+
tracesSampleRate: 1,
12+
});

0 commit comments

Comments
 (0)