Skip to content

Commit 86e0cd1

Browse files
committed
feat(replay): Allow skipping the final flush on stop() via { flush: false }
The public `ReplayIntegration.stop()` always force-flushes the pending segment when recording in `session` mode, with no way to opt out. Since Replay envelopes bypass `beforeSend`, consent-gated setups that call `stop()` on consent withdrawal end up sending the buffered segment after the user revoked consent. Expose the existing internal `forceFlush` control on the public API as `stop({ flush?: boolean })`. It defaults to the current behavior (flush when `recordingMode === 'session'`), so the change is fully backwards compatible. `flush: false` stops recording without sending the pending segment. Closes #22220
1 parent c9716eb commit 86e0cd1

3 files changed

Lines changed: 55 additions & 2 deletions

File tree

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+
- feat(replay): Allow skipping the final flush when stopping recording via `stop({ flush: false })` ([#PR_NUMBER](https://github.com/getsentry/sentry-javascript/pull/PR_NUMBER))
8+
79
Work in this release was contributed by @dobladov and @PeterWadie. Thank you for your contributions!
810

911
## 10.65.0

packages/replay-internal/src/integration.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -298,13 +298,25 @@ export class Replay implements Integration {
298298
/**
299299
* Currently, this needs to be manually called (e.g. for tests). Sentry SDK
300300
* does not support a teardown
301+
*
302+
* @param options.flush - Whether to flush the pending replay segment when stopping.
303+
* When recording in `session` mode, `stop()` flushes the buffered-but-unsent segment by
304+
* default (`flush: true`), matching the previous behavior. Pass `flush: false` to stop
305+
* recording without sending that pending segment — useful when a user withdraws consent
306+
* and no further data should leave the browser.
307+
*
308+
* Note: `flush: false` only prevents the *pending* (final) segment from being sent. It does
309+
* **not** retract segments that were already sent earlier during `session` recording.
301310
*/
302-
public stop(): Promise<void> {
311+
public stop(options?: { flush?: boolean }): Promise<void> {
303312
if (!this._replay) {
304313
return Promise.resolve();
305314
}
306315

307-
return this._replay.stop({ forceFlush: this._replay.recordingMode === 'session', reason: 'manual' });
316+
return this._replay.stop({
317+
forceFlush: options?.flush ?? this._replay.recordingMode === 'session',
318+
reason: 'manual',
319+
});
308320
}
309321

310322
/**

packages/replay-internal/test/integration/stop.test.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,45 @@ describe('Integration | stop', () => {
153153
});
154154
});
155155

156+
it('does not flush the pending segment when stopped with `{ flush: false }` in session mode', async function () {
157+
// Default mock SDK records in `session` mode (replaysSessionSampleRate: 1.0)
158+
expect(replay.recordingMode).toBe('session');
159+
160+
const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP });
161+
addEvent(replay, TEST_EVENT, true);
162+
expect(replay.eventBuffer?.hasEvents).toBe(true);
163+
expect(mockRunFlush).toHaveBeenCalledTimes(0);
164+
165+
// stop without force-flushing the pending segment (e.g. on consent withdrawal)
166+
await integration.stop({ flush: false });
167+
168+
// the buffered segment must not have been flushed/sent
169+
expect(mockRunFlush).toHaveBeenCalledTimes(0);
170+
expect(replay).not.toHaveLastSentReplay();
171+
172+
// recording is still torn down as usual
173+
expect(replay.eventBuffer).toBe(null);
174+
expect(replay.session).toEqual(undefined);
175+
});
176+
177+
it('flushes the pending segment when stopped with `{ flush: true }` in session mode', async function () {
178+
expect(replay.recordingMode).toBe('session');
179+
180+
const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP });
181+
addEvent(replay, TEST_EVENT, true);
182+
expect(replay.eventBuffer?.hasEvents).toBe(true);
183+
expect(mockRunFlush).toHaveBeenCalledTimes(0);
184+
185+
// explicitly force-flush the pending segment
186+
await integration.stop({ flush: true });
187+
188+
expect(mockRunFlush).toHaveBeenCalledTimes(1);
189+
expect(replay.eventBuffer).toBe(null);
190+
expect(replay).toHaveLastSentReplay({
191+
recordingData: JSON.stringify([TEST_EVENT]),
192+
});
193+
});
194+
156195
it('does not call core SDK `addClickKeypressInstrumentationHandler` after initial setup', async function () {
157196
// NOTE: We clear mockAddDomInstrumentationHandler after every test
158197
await integration.stop();

0 commit comments

Comments
 (0)