Skip to content

Commit 0053359

Browse files
committed
add startBuffering, change recordingMode from error to buffer, some fixes
1 parent c94f3e3 commit 0053359

File tree

18 files changed

+339
-55
lines changed

18 files changed

+339
-55
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
window.Replay = new Sentry.Replay({
5+
flushMinDelay: 1000,
6+
flushMaxDelay: 1000,
7+
});
8+
9+
Sentry.init({
10+
dsn: 'https://[email protected]/1337',
11+
sampleRate: 1,
12+
replaysSessionSampleRate: 0.0,
13+
replaysOnErrorSampleRate: 0.0,
14+
15+
integrations: [window.Replay],
16+
});
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
document.getElementById('go-background').addEventListener('click', () => {
2+
Object.defineProperty(document, 'hidden', { value: true, writable: true });
3+
const ev = document.createEvent('Event');
4+
ev.initEvent('visibilitychange');
5+
document.dispatchEvent(ev);
6+
});
7+
8+
document.getElementById('error').addEventListener('click', () => {
9+
throw new Error('Ooops');
10+
});
11+
12+
document.getElementById('error2').addEventListener('click', () => {
13+
throw new Error('Another error');
14+
});
15+
16+
document.getElementById('drop').addEventListener('click', () => {
17+
throw new Error('[drop] Ooops');
18+
});
19+
20+
document.getElementById('log').addEventListener('click', () => {
21+
console.log('Some message');
22+
});
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8" />
5+
</head>
6+
<body>
7+
<button id="go-background">Go to background</button>
8+
<button id="error">Throw Error</button>
9+
<button id="error2">Another Error</button>
10+
<button id="log">Log stuff to the console</button>
11+
</body>
12+
</html>
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { expect } from '@playwright/test';
2+
import type { Replay } from '@sentry/replay';
3+
import type { ReplayContainer } from '@sentry/replay/build/npm/types/types';
4+
5+
import { sentryTest } from '../../../utils/fixtures';
6+
import { envelopeRequestParser, waitForErrorRequest } from '../../../utils/helpers';
7+
import {
8+
expectedClickBreadcrumb,
9+
expectedConsoleBreadcrumb,
10+
getExpectedReplayEvent,
11+
} from '../../../utils/replayEventTemplates';
12+
import {
13+
getReplayEvent,
14+
getReplayRecordingContent,
15+
isReplayEvent,
16+
shouldSkipReplayTest,
17+
waitForReplayRequest,
18+
} from '../../../utils/replayHelpers';
19+
20+
sentryTest(
21+
'[buffer-mode] manually start buffer mode and capture buffer',
22+
async ({ getLocalTestPath, page, browserName }) => {
23+
// This was sometimes flaky on firefox/webkit, so skipping for now
24+
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
25+
sentryTest.skip();
26+
}
27+
28+
let callsToSentry = 0;
29+
let errorEventId: string | undefined;
30+
const reqPromise0 = waitForReplayRequest(page, 0);
31+
const reqPromise1 = waitForReplayRequest(page, 1);
32+
// const reqPromise2 = waitForReplayRequest(page, 2);
33+
const reqErrorPromise = waitForErrorRequest(page);
34+
35+
await page.route('https://dsn.ingest.sentry.io/**/*', route => {
36+
const event = envelopeRequestParser(route.request());
37+
// error events have no type field
38+
if (event && !event.type && event.event_id) {
39+
errorEventId = event.event_id;
40+
}
41+
// We only want to count errors & replays here
42+
if (event && (!event.type || isReplayEvent(event))) {
43+
callsToSentry++;
44+
}
45+
46+
return route.fulfill({
47+
status: 200,
48+
contentType: 'application/json',
49+
body: JSON.stringify({ id: 'test-id' }),
50+
});
51+
});
52+
53+
const url = await getLocalTestPath({ testDir: __dirname });
54+
55+
await page.goto(url);
56+
await page.click('#go-background');
57+
await page.click('#error');
58+
await new Promise(resolve => setTimeout(resolve, 1000));
59+
60+
// error, no replays
61+
expect(callsToSentry).toEqual(1);
62+
await reqErrorPromise;
63+
64+
expect(
65+
await page.evaluate(() => {
66+
const replayIntegration = (window as unknown as Window & { Replay: { _replay: ReplayContainer } }).Replay;
67+
const replay = replayIntegration._replay;
68+
return replay.isEnabled();
69+
}),
70+
).toBe(false);
71+
72+
// Start buffering and assert that it is enabled
73+
expect(
74+
await page.evaluate(() => {
75+
const replayIntegration = (window as unknown as Window & { Replay: InstanceType<typeof Replay> }).Replay;
76+
// @ts-ignore private
77+
const replay = replayIntegration._replay;
78+
replayIntegration.startBuffering();
79+
return replay.isEnabled();
80+
}),
81+
).toBe(true);
82+
83+
await page.click('#log');
84+
await page.click('#go-background');
85+
await page.click('#error2');
86+
await new Promise(resolve => setTimeout(resolve, 1000));
87+
88+
// 2 errors
89+
expect(callsToSentry).toEqual(2);
90+
91+
await page.evaluate(async () => {
92+
const replayIntegration = (window as unknown as Window & { Replay: Replay }).Replay;
93+
await replayIntegration.flush();
94+
});
95+
96+
const req0 = await reqPromise0;
97+
98+
// 2 errors, 1 flush
99+
expect(callsToSentry).toEqual(3);
100+
101+
await page.click('#log');
102+
await page.click('#go-background');
103+
await new Promise(resolve => setTimeout(resolve, 1000));
104+
105+
// Switches to session mode
106+
expect(callsToSentry).toEqual(4);
107+
const req1 = await reqPromise1;
108+
109+
const event0 = getReplayEvent(req0);
110+
const content0 = getReplayRecordingContent(req0);
111+
112+
const event1 = getReplayEvent(req1);
113+
const content1 = getReplayRecordingContent(req1);
114+
115+
expect(event0).toEqual(
116+
getExpectedReplayEvent({
117+
contexts: { replay: { error_sample_rate: 0, session_sample_rate: 0 } },
118+
error_ids: [errorEventId!],
119+
replay_type: 'buffer',
120+
}),
121+
);
122+
123+
// The first event should have both, full and incremental snapshots,
124+
// as we recorded and kept all events in the buffer
125+
expect(content0.fullSnapshots).toHaveLength(1);
126+
// We don't know how many incremental snapshots we'll have (also browser-dependent),
127+
// but we know that we have at least 5
128+
expect(content0.incrementalSnapshots.length).toBeGreaterThan(5);
129+
// We want to make sure that the event that triggered the error was recorded.
130+
expect(content0.breadcrumbs).toEqual(
131+
expect.arrayContaining([
132+
{
133+
...expectedClickBreadcrumb,
134+
message: 'body > button#error2',
135+
data: {
136+
nodeId: expect.any(Number),
137+
node: {
138+
attributes: {
139+
id: 'error2',
140+
},
141+
id: expect.any(Number),
142+
tagName: 'button',
143+
textContent: '******* *****',
144+
},
145+
},
146+
},
147+
]),
148+
);
149+
150+
expect(event1).toEqual(
151+
getExpectedReplayEvent({
152+
contexts: { replay: { error_sample_rate: 0, session_sample_rate: 0 } },
153+
replay_type: 'buffer', // although we're in session mode, we still send 'buffer' as replay_type
154+
segment_id: 1,
155+
urls: [],
156+
}),
157+
);
158+
159+
//
160+
expect(content1.fullSnapshots).toHaveLength(0);
161+
162+
expect(content1.breadcrumbs).toEqual(
163+
expect.arrayContaining([
164+
{
165+
...expectedClickBreadcrumb,
166+
message: 'body > button#log',
167+
data: {
168+
node: {
169+
attributes: { id: 'log' },
170+
id: expect.any(Number),
171+
tagName: 'button',
172+
textContent: '*** ***** ** *** *******',
173+
},
174+
nodeId: expect.any(Number),
175+
},
176+
},
177+
{ ...expectedConsoleBreadcrumb, level: 'log', message: 'Some message' },
178+
]),
179+
);
180+
},
181+
);

packages/browser-integration-tests/suites/replay/errors/droppedError/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,6 @@ sentryTest(
4242
expect(callsToSentry).toEqual(0);
4343

4444
const replay = await getReplaySnapshot(page);
45-
expect(replay.recordingMode).toBe('error');
45+
expect(replay.recordingMode).toBe('buffer');
4646
},
4747
);

packages/browser-integration-tests/suites/replay/errors/errorMode/test.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {
1616
} from '../../../../utils/replayHelpers';
1717

1818
sentryTest(
19-
'[error-mode] should start recording and switch to session mode once an error is thrown',
19+
'[error-mode] should start recording, only sample the 2nd error, and switch to session mode once an error is thrown',
2020
async ({ getLocalTestPath, page, browserName }) => {
2121
// This was sometimes flaky on firefox/webkit, so skipping for now
2222
if (shouldSkipReplayTest() || ['firefox', 'webkit'].includes(browserName)) {
@@ -59,6 +59,8 @@ sentryTest(
5959
await page.click('#error');
6060
const req0 = await reqPromise0;
6161

62+
expect(callsToSentry).toEqual(2); // 1 error, 1 replay event
63+
6264
await page.click('#go-background');
6365
const req1 = await reqPromise1;
6466
await reqErrorPromise;
@@ -84,7 +86,7 @@ sentryTest(
8486
getExpectedReplayEvent({
8587
contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } },
8688
error_ids: [errorEventId!],
87-
replay_type: 'error',
89+
replay_type: 'buffer',
8890
}),
8991
);
9092

@@ -118,7 +120,7 @@ sentryTest(
118120
expect(event1).toEqual(
119121
getExpectedReplayEvent({
120122
contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } },
121-
replay_type: 'error', // although we're in session mode, we still send 'error' as replay_type
123+
replay_type: 'buffer', // although we're in session mode, we still send 'error' as replay_type
122124
replay_start_timestamp: undefined,
123125
segment_id: 1,
124126
urls: [],
@@ -134,7 +136,7 @@ sentryTest(
134136
expect(event2).toEqual(
135137
getExpectedReplayEvent({
136138
contexts: { replay: { error_sample_rate: 1, session_sample_rate: 0 } },
137-
replay_type: 'error',
139+
replay_type: 'buffer',
138140
replay_start_timestamp: undefined,
139141
segment_id: 2,
140142
urls: [],

packages/browser-integration-tests/suites/replay/errors/errorNotSent/test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,6 @@ sentryTest(
3636
expect(callsToSentry).toEqual(1);
3737

3838
const replay = await getReplaySnapshot(page);
39-
expect(replay.recordingMode).toBe('error');
39+
expect(replay.recordingMode).toBe('buffer');
4040
},
4141
);

packages/replay/src/coreHandlers/handleAfterSendEvent.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { Event, Transport, TransportMakeRequestResponse } from '@sentry/typ
44
import { UNABLE_TO_SEND_REPLAY } from '../constants';
55
import type { ReplayContainer } from '../types';
66
import { isErrorEvent, isTransactionEvent } from '../util/eventUtils';
7+
import { isSampled } from '../util/isSampled';
78

89
type AfterSendEventCallback = (event: Event, sendResponse: TransportMakeRequestResponse | void) => void;
910

@@ -49,10 +50,14 @@ export function handleAfterSendEvent(replay: ReplayContainer): AfterSendEventCal
4950
// Trigger error recording
5051
// Need to be very careful that this does not cause an infinite loop
5152
if (
52-
replay.recordingMode === 'error' &&
53+
replay.recordingMode === 'buffer' &&
5354
event.exception &&
5455
event.message !== UNABLE_TO_SEND_REPLAY // ignore this error because otherwise we could loop indefinitely with trying to capture replay and failing
5556
) {
57+
if (!isSampled(replay.getOptions().errorSampleRate)) {
58+
return;
59+
}
60+
5661
setTimeout(() => {
5762
// Capture current event buffer as new replay
5863
void replay.sendBufferedReplayOrFlush();

packages/replay/src/integration.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,10 +181,11 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
181181
}
182182

183183
/**
184-
* Initializes the plugin.
184+
* Start a replay regardless of sampling rate. Calling this will always
185+
* create a new session. Will throw an error if replay is already in progress.
185186
*
186187
* Creates or loads a session, attaches listeners to varying events (DOM,
187-
* PerformanceObserver, Recording, Sentry SDK, etc)
188+
* _performanceObserver, Recording, Sentry SDK, etc)
188189
*/
189190
public start(): void {
190191
if (!this._replay) {
@@ -194,6 +195,18 @@ Sentry.init({ replaysOnErrorSampleRate: ${errorSampleRate} })`,
194195
this._replay.start();
195196
}
196197

198+
/**
199+
* Start replay buffering. Buffers until `flush()` is called or, if
200+
* `replaysOnErrorSampleRate` > 0, until an error occurs.
201+
*/
202+
public startBuffering(): void {
203+
if (!this._replay) {
204+
return;
205+
}
206+
207+
this._replay.startBuffering();
208+
}
209+
197210
/**
198211
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
199212
* does not support a teardown

0 commit comments

Comments
 (0)