Skip to content

Commit 963a936

Browse files
authored
Merge branch 'develop' into sig/truncation-changes
2 parents 5ae0c52 + 455c231 commit 963a936

File tree

67 files changed

+795
-151
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+795
-151
lines changed

.size-limit.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ module.exports = [
157157
name: 'CDN Bundle',
158158
path: createCDNPath('bundle.min.js'),
159159
gzip: true,
160-
limit: '27 KB',
160+
limit: '27.5 KB',
161161
},
162162
{
163163
name: 'CDN Bundle (incl. Tracing)',

dev-packages/browser-integration-tests/suites/integrations/supabase/auth/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ sentryTest('should capture Supabase authentication errors', async ({ getLocalTes
143143
start_timestamp: expect.any(Number),
144144
timestamp: expect.any(Number),
145145
trace_id: transactionEvent.contexts?.trace?.trace_id,
146-
status: 'unknown_error',
146+
status: 'internal_error',
147147
data: expect.objectContaining({
148148
'sentry.op': 'db',
149149
'sentry.origin': 'auto.db.supabase',
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = Sentry.replayIntegration({
5+
flushMinDelay: 200,
6+
flushMaxDelay: 200,
7+
minReplayDuration: 0,
8+
stickySession: true,
9+
});
10+
11+
Sentry.init({
12+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
13+
sampleRate: 1,
14+
replaysSessionSampleRate: 0.0,
15+
replaysOnErrorSampleRate: 1.0,
16+
17+
integrations: [window.Replay],
18+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
document.getElementById('error1').addEventListener('click', () => {
2+
throw new Error('First Error');
3+
});
4+
5+
document.getElementById('error2').addEventListener('click', () => {
6+
throw new Error('Second Error');
7+
});
8+
9+
document.getElementById('click').addEventListener('click', () => {
10+
// Just a click for interaction
11+
});
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="error1">Throw First Error</button>
8+
<button id="error2">Throw Second Error</button>
9+
<button id="click">Click me</button>
10+
</body>
11+
</html>
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../utils/fixtures';
3+
import { envelopeRequestParser, waitForErrorRequest } from '../../../utils/helpers';
4+
import {
5+
getReplaySnapshot,
6+
isReplayEvent,
7+
shouldSkipReplayTest,
8+
waitForReplayRunning,
9+
} from '../../../utils/replayHelpers';
10+
11+
sentryTest(
12+
'buffer mode remains after interrupting error event ingest',
13+
async ({ getLocalTestUrl, page, browserName }) => {
14+
if (shouldSkipReplayTest() || browserName === 'webkit') {
15+
sentryTest.skip();
16+
}
17+
18+
let errorCount = 0;
19+
let replayCount = 0;
20+
const errorEventIds: string[] = [];
21+
const replayIds: string[] = [];
22+
let firstReplayEventResolved: (value?: unknown) => void = () => {};
23+
// Need TS 5.7 for withResolvers
24+
const firstReplayEventPromise = new Promise(resolve => {
25+
firstReplayEventResolved = resolve;
26+
});
27+
28+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
29+
30+
await page.route('https://dsn.ingest.sentry.io/**/*', async route => {
31+
const event = envelopeRequestParser(route.request());
32+
33+
// Track error events
34+
if (event && !event.type && event.event_id) {
35+
errorCount++;
36+
errorEventIds.push(event.event_id);
37+
if (event.tags?.replayId) {
38+
replayIds.push(event.tags.replayId as string);
39+
40+
if (errorCount === 1) {
41+
firstReplayEventResolved();
42+
// intentional so that it never resolves, we'll force a reload instead to interrupt the normal flow
43+
await new Promise(resolve => setTimeout(resolve, 100000));
44+
}
45+
}
46+
}
47+
48+
// Track replay events and simulate failure for the first replay
49+
if (event && isReplayEvent(event)) {
50+
replayCount++;
51+
}
52+
53+
// Success for other requests
54+
return route.fulfill({
55+
status: 200,
56+
contentType: 'application/json',
57+
body: JSON.stringify({ id: 'test-id' }),
58+
});
59+
});
60+
61+
await page.goto(url);
62+
63+
// Wait for replay to initialize
64+
await waitForReplayRunning(page);
65+
66+
waitForErrorRequest(page);
67+
await page.locator('#error1').click();
68+
69+
// This resolves, but the route doesn't get fulfilled as we want the reload to "interrupt" this flow
70+
await firstReplayEventPromise;
71+
expect(errorCount).toBe(1);
72+
expect(replayCount).toBe(0);
73+
expect(replayIds).toHaveLength(1);
74+
75+
const firstSession = await getReplaySnapshot(page);
76+
const firstSessionId = firstSession.session?.id;
77+
expect(firstSessionId).toBeDefined();
78+
expect(firstSession.session?.sampled).toBe('buffer');
79+
expect(firstSession.session?.dirty).toBe(true);
80+
expect(firstSession.recordingMode).toBe('buffer');
81+
82+
await page.reload();
83+
const secondSession = await getReplaySnapshot(page);
84+
expect(secondSession.session?.sampled).toBe('buffer');
85+
expect(secondSession.session?.dirty).toBe(true);
86+
expect(secondSession.recordingMode).toBe('buffer');
87+
expect(secondSession.session?.id).toBe(firstSessionId);
88+
expect(secondSession.session?.segmentId).toBe(0);
89+
},
90+
);
91+
92+
sentryTest('buffer mode remains after interrupting replay flush', async ({ getLocalTestUrl, page, browserName }) => {
93+
if (shouldSkipReplayTest() || browserName === 'webkit') {
94+
sentryTest.skip();
95+
}
96+
97+
let errorCount = 0;
98+
let replayCount = 0;
99+
const errorEventIds: string[] = [];
100+
const replayIds: string[] = [];
101+
let firstReplayEventResolved: (value?: unknown) => void = () => {};
102+
// Need TS 5.7 for withResolvers
103+
const firstReplayEventPromise = new Promise(resolve => {
104+
firstReplayEventResolved = resolve;
105+
});
106+
107+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
108+
109+
await page.route('https://dsn.ingest.sentry.io/**/*', async route => {
110+
const event = envelopeRequestParser(route.request());
111+
112+
// Track error events
113+
if (event && !event.type && event.event_id) {
114+
errorCount++;
115+
errorEventIds.push(event.event_id);
116+
if (event.tags?.replayId) {
117+
replayIds.push(event.tags.replayId as string);
118+
}
119+
}
120+
121+
// Track replay events and simulate failure for the first replay
122+
if (event && isReplayEvent(event)) {
123+
replayCount++;
124+
if (replayCount === 1) {
125+
firstReplayEventResolved();
126+
// intentional so that it never resolves, we'll force a reload instead to interrupt the normal flow
127+
await new Promise(resolve => setTimeout(resolve, 100000));
128+
}
129+
}
130+
131+
// Success for other requests
132+
return route.fulfill({
133+
status: 200,
134+
contentType: 'application/json',
135+
body: JSON.stringify({ id: 'test-id' }),
136+
});
137+
});
138+
139+
await page.goto(url);
140+
141+
// Wait for replay to initialize
142+
await waitForReplayRunning(page);
143+
144+
await page.locator('#error1').click();
145+
await firstReplayEventPromise;
146+
expect(errorCount).toBe(1);
147+
expect(replayCount).toBe(1);
148+
expect(replayIds).toHaveLength(1);
149+
150+
// Get the first session info
151+
const firstSession = await getReplaySnapshot(page);
152+
const firstSessionId = firstSession.session?.id;
153+
expect(firstSessionId).toBeDefined();
154+
expect(firstSession.session?.sampled).toBe('buffer');
155+
expect(firstSession.session?.dirty).toBe(true);
156+
expect(firstSession.recordingMode).toBe('buffer'); // But still in buffer mode
157+
158+
await page.reload();
159+
await waitForReplayRunning(page);
160+
const secondSession = await getReplaySnapshot(page);
161+
expect(secondSession.session?.sampled).toBe('buffer');
162+
expect(secondSession.session?.dirty).toBe(true);
163+
expect(secondSession.session?.id).toBe(firstSessionId);
164+
expect(secondSession.session?.segmentId).toBe(1);
165+
// Because a flush attempt was made and not allowed to complete, segmentId increased from 0,
166+
// so we resume in session mode
167+
expect(secondSession.recordingMode).toBe('session');
168+
});
169+
170+
sentryTest(
171+
'starts a new session after interrupting replay flush and session "expires"',
172+
async ({ getLocalTestUrl, page, browserName }) => {
173+
if (shouldSkipReplayTest() || browserName === 'webkit') {
174+
sentryTest.skip();
175+
}
176+
177+
let errorCount = 0;
178+
let replayCount = 0;
179+
const errorEventIds: string[] = [];
180+
const replayIds: string[] = [];
181+
let firstReplayEventResolved: (value?: unknown) => void = () => {};
182+
// Need TS 5.7 for withResolvers
183+
const firstReplayEventPromise = new Promise(resolve => {
184+
firstReplayEventResolved = resolve;
185+
});
186+
187+
const url = await getLocalTestUrl({ testDir: __dirname, skipDsnRouteHandler: true });
188+
189+
await page.route('https://dsn.ingest.sentry.io/**/*', async route => {
190+
const event = envelopeRequestParser(route.request());
191+
192+
// Track error events
193+
if (event && !event.type && event.event_id) {
194+
errorCount++;
195+
errorEventIds.push(event.event_id);
196+
if (event.tags?.replayId) {
197+
replayIds.push(event.tags.replayId as string);
198+
}
199+
}
200+
201+
// Track replay events and simulate failure for the first replay
202+
if (event && isReplayEvent(event)) {
203+
replayCount++;
204+
if (replayCount === 1) {
205+
firstReplayEventResolved();
206+
// intentional so that it never resolves, we'll force a reload instead to interrupt the normal flow
207+
await new Promise(resolve => setTimeout(resolve, 100000));
208+
}
209+
}
210+
211+
// Success for other requests
212+
return route.fulfill({
213+
status: 200,
214+
contentType: 'application/json',
215+
body: JSON.stringify({ id: 'test-id' }),
216+
});
217+
});
218+
219+
await page.goto(url);
220+
221+
// Wait for replay to initialize
222+
await waitForReplayRunning(page);
223+
224+
// Trigger first error - this should change session sampled to "session"
225+
await page.locator('#error1').click();
226+
await firstReplayEventPromise;
227+
expect(errorCount).toBe(1);
228+
expect(replayCount).toBe(1);
229+
expect(replayIds).toHaveLength(1);
230+
231+
// Get the first session info
232+
const firstSession = await getReplaySnapshot(page);
233+
const firstSessionId = firstSession.session?.id;
234+
expect(firstSessionId).toBeDefined();
235+
expect(firstSession.session?.sampled).toBe('buffer');
236+
expect(firstSession.session?.dirty).toBe(true);
237+
expect(firstSession.recordingMode).toBe('buffer'); // But still in buffer mode
238+
239+
// Now expire the session by manipulating session storage
240+
// Simulate session expiry by setting lastActivity to a time in the past
241+
await page.evaluate(() => {
242+
const replayIntegration = (window as any).Replay;
243+
const replay = replayIntegration['_replay'];
244+
245+
// Set session as expired (15 minutes ago)
246+
if (replay.session) {
247+
const fifteenMinutesAgo = Date.now() - 15 * 60 * 1000;
248+
replay.session.lastActivity = fifteenMinutesAgo;
249+
replay.session.started = fifteenMinutesAgo;
250+
251+
// Also update session storage if sticky sessions are enabled
252+
const sessionKey = 'sentryReplaySession';
253+
const sessionData = sessionStorage.getItem(sessionKey);
254+
if (sessionData) {
255+
const session = JSON.parse(sessionData);
256+
session.lastActivity = fifteenMinutesAgo;
257+
session.started = fifteenMinutesAgo;
258+
sessionStorage.setItem(sessionKey, JSON.stringify(session));
259+
}
260+
}
261+
});
262+
263+
await page.reload();
264+
const secondSession = await getReplaySnapshot(page);
265+
expect(secondSession.session?.sampled).toBe('buffer');
266+
expect(secondSession.recordingMode).toBe('buffer');
267+
expect(secondSession.session?.id).not.toBe(firstSessionId);
268+
expect(secondSession.session?.segmentId).toBe(0);
269+
},
270+
);
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
Sentry.init({
6+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
7+
integrations: [
8+
Sentry.browserTracingIntegration({
9+
idleTimeout: 1000,
10+
enableLongTask: false,
11+
enableInp: true,
12+
instrumentPageLoad: false,
13+
instrumentNavigation: false,
14+
}),
15+
],
16+
tracesSampleRate: 1,
17+
});
18+
19+
const client = Sentry.getClient();
20+
21+
// Force page load transaction name to a testable value
22+
Sentry.startBrowserTracingPageLoadSpan(client, {
23+
name: 'test-url',
24+
attributes: {
25+
[Sentry.SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'url',
26+
},
27+
});

0 commit comments

Comments
 (0)