Skip to content

Commit 4b663d6

Browse files
authored
Merge branch 'develop' into feat/monitor-trace
2 parents 4dfa0af + 94190f8 commit 4b663d6

File tree

96 files changed

+1468
-409
lines changed

Some content is hidden

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

96 files changed

+1468
-409
lines changed

.cursor/rules/publishing_release.mdc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,18 @@ Use these guidelines when publishing a new Sentry JavaScript SDK release.
1212

1313
The release process is outlined in [publishing-a-release.md](mdc:docs/publishing-a-release.md).
1414

15-
1. Make sure you are on the latest version of the `develop` branch. To confirm this, run `git pull origin develop` to get the latest changes from the repo.
15+
1. Ensure you're on the `develop` branch with the latest changes:
16+
- If you have unsaved changes, stash them with `git stash -u`.
17+
- If you're on a different branch than `develop`, check out the develop branch using `git checkout develop`.
18+
- Pull the latest updates from the remote repository by running `git pull origin develop`.
19+
1620
2. Run `yarn changelog` on the `develop` branch and copy the output. You can use `yarn changelog | pbcopy` to copy the output of `yarn changelog` into your clipboard.
1721
3. Decide on a version for the release based on [semver](mdc:https://semver.org). The version should be decided based on what is in included in the release. For example, if the release includes a new feature, we should increment the minor version. If it includes only bug fixes, we should increment the patch version. You can find the latest version in [CHANGELOG.md](mdc:CHANGELOG.md) at the very top.
1822
4. Create a branch `prepare-release/VERSION`, eg. `prepare-release/8.1.0`, off `develop`.
19-
5. Update [CHANGELOG.md](mdc:CHANGELOG.md) to add an entry for the next release number and a list of changes since the last release from the output of `yarn changelog`. See the `Updating the Changelog` section in [publishing-a-release.md](mdc:docs/publishing-a-release.md) for more details. If you remove changelog entries because they are not applicable, please let the user know.
23+
5. Update [CHANGELOG.md](mdc:CHANGELOG.md) to add an entry for the next release number and a list of changes since the last release from the output of `yarn changelog`. See the `Updating the Changelog` section in [publishing-a-release.md](mdc:docs/publishing-a-release.md) for more details. Do not remove any changelog entries.
2024
6. Commit the changes to [CHANGELOG.md](mdc:CHANGELOG.md) with `meta(changelog): Update changelog for VERSION` where `VERSION` is the version of the release, e.g. `meta(changelog): Update changelog for 8.1.0`
2125
7. Push the `prepare-release/VERSION` branch to origin and remind the user that the release PR needs to be opened from the `master` branch.
26+
8. In case you were working on a different branch, you can checkout back to the branch you were working on and continue your work by unstashing the changes you stashed earlier with the command `git stash pop` (only if you stashed changes).
2227

2328
## Key Commands
2429

.size-limit.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ module.exports = [
3838
path: 'packages/browser/build/npm/esm/index.js',
3939
import: createImport('init', 'browserTracingIntegration'),
4040
gzip: true,
41-
limit: '41 KB',
41+
limit: '41.3 KB',
4242
},
4343
{
4444
name: '@sentry/browser (incl. Tracing, Profiling)',
@@ -127,7 +127,7 @@ module.exports = [
127127
import: createImport('init', 'ErrorBoundary', 'reactRouterV6BrowserTracingIntegration'),
128128
ignore: ['react/jsx-runtime'],
129129
gzip: true,
130-
limit: '43 KB',
130+
limit: '43.3 KB',
131131
},
132132
// Vue SDK (ESM)
133133
{
@@ -142,7 +142,7 @@ module.exports = [
142142
path: 'packages/vue/build/esm/index.js',
143143
import: createImport('init', 'browserTracingIntegration'),
144144
gzip: true,
145-
limit: '43 KB',
145+
limit: '43.1 KB',
146146
},
147147
// Svelte SDK (ESM)
148148
{
@@ -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)',
@@ -190,7 +190,7 @@ module.exports = [
190190
path: createCDNPath('bundle.tracing.min.js'),
191191
gzip: false,
192192
brotli: false,
193-
limit: '124 KB',
193+
limit: '124.1 KB',
194194
},
195195
{
196196
name: 'CDN Bundle (incl. Tracing, Replay) - uncompressed',

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
Work in this release was contributed by @hanseo0507. Thank you for your contribution!
8+
79
## 10.22.0
810

911
### Important Changes

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+
);

0 commit comments

Comments
 (0)