Skip to content

Commit 5021ea0

Browse files
committed
feat(replay): Keep last two checkouts in error mode
1 parent 759f07c commit 5021ea0

File tree

8 files changed

+151
-22
lines changed

8 files changed

+151
-22
lines changed

packages/replay/src/eventBuffer/EventBufferArray.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export class EventBufferArray implements EventBuffer {
2626

2727
/** @inheritdoc */
2828
public destroy(): void {
29-
this._events = [];
29+
this._clear();
3030
}
3131

3232
/** @inheritdoc */
@@ -36,8 +36,8 @@ export class EventBufferArray implements EventBuffer {
3636
}
3737

3838
/** @inheritdoc */
39-
public async clear(): Promise<void> {
40-
this._events = [];
39+
public async clear(untilPos?: number): Promise<void> {
40+
this._clear(untilPos);
4141
}
4242

4343
/** @inheritdoc */
@@ -47,8 +47,17 @@ export class EventBufferArray implements EventBuffer {
4747
// events member so that we do not lose new events while uploading
4848
// attachment.
4949
const eventsRet = this._events;
50-
this._events = [];
5150
resolve(JSON.stringify(eventsRet));
51+
this._clear();
5252
});
5353
}
54+
55+
/** Clear all events. */
56+
private _clear(untilPos?: number): void {
57+
if (untilPos) {
58+
this._events.splice(0, untilPos);
59+
} else {
60+
this._events = [];
61+
}
62+
}
5463
}

packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,15 @@ export class EventBufferCompressionWorker implements EventBuffer {
1212
* For example, page is reloaded and a flush attempt is made, but
1313
* `finish()` (and thus the flush), does not complete.
1414
*/
15-
public _pendingEvents: RecordingEvent[] = [];
15+
public _pendingEvents: RecordingEvent[];
1616

1717
private _worker: Worker;
1818
private _eventBufferItemLength: number = 0;
1919
private _id: number = 0;
2020

2121
public constructor(worker: Worker) {
2222
this._worker = worker;
23+
this._pendingEvents = [];
2324
}
2425

2526
/**
@@ -85,7 +86,11 @@ export class EventBufferCompressionWorker implements EventBuffer {
8586
}
8687

8788
/** @inheritdoc */
88-
public clear(): Promise<void> {
89+
public clear(untilPos?: number): Promise<void> {
90+
this._clear(untilPos);
91+
92+
// TODO FN: Clear up to pos
93+
8994
// This will clear the queue of events that are waiting to be compressed
9095
return this._postMessage({
9196
id: this._getAndIncrementId(),
@@ -98,9 +103,22 @@ export class EventBufferCompressionWorker implements EventBuffer {
98103
* Finish the event buffer and return the compressed data.
99104
*/
100105
public finish(): Promise<Uint8Array> {
106+
this._clear();
107+
101108
return this._finishRequest(this._getAndIncrementId());
102109
}
103110

111+
/**
112+
* Clear all pending events up to the given event pos.
113+
*/
114+
private _clear(untilPos?: number): void {
115+
if (untilPos) {
116+
this._pendingEvents.splice(0, untilPos);
117+
} else {
118+
this._pendingEvents = [];
119+
}
120+
}
121+
104122
/**
105123
* Post message to worker and wait for response before resolving promise.
106124
*/

packages/replay/src/eventBuffer/EventBufferProxy.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,8 @@ export class EventBufferProxy implements EventBuffer {
4545
}
4646

4747
/** @inheritdoc */
48-
public clear(): Promise<void> {
49-
return this._used.clear();
48+
public clear(untilPos?: number): Promise<void> {
49+
return this._used.clear(untilPos);
5050
}
5151

5252
/** @inheritDoc */

packages/replay/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ export interface EventBuffer {
210210
*/
211211
readonly pendingEvents: RecordingEvent[];
212212

213+
/**
214+
* The pos. in pendingEvents of the last checkout event.
215+
* Can be used to only clear part of the queue.
216+
*/
217+
lastCheckoutEventPos?: number;
218+
213219
/**
214220
* Destroy the event buffer.
215221
*/
@@ -224,8 +230,9 @@ export interface EventBuffer {
224230

225231
/**
226232
* Clear any pending events from the buffer.
233+
* If `untilPos` is given, only events up to that position will be cleared.
227234
*/
228-
clear(): Promise<void>;
235+
clear(untilPos?: number): Promise<void>;
229236

230237
/**
231238
* Clears and returns the contents of the buffer.

packages/replay/src/util/addEvent.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ export async function addEvent(
99
event: RecordingEvent,
1010
isCheckout?: boolean,
1111
): Promise<AddEventResult | null> {
12-
const { eventBuffer } = replay;
12+
const { eventBuffer, session } = replay;
1313

1414
if (!eventBuffer) {
1515
// This implies that `_isEnabled` is false
1616
return null;
1717
}
1818

19-
if (replay.isPaused()) {
19+
if (replay.isPaused() || !session) {
2020
// Do not add to event buffer when recording is paused
2121
return null;
2222
}
@@ -37,12 +37,24 @@ export async function addEvent(
3737
// Only record earliest event if a new session was created, otherwise it
3838
// shouldn't be relevant
3939
const earliestEvent = replay.getContext().earliestEvent;
40-
if (replay.session && replay.session.segmentId === 0 && (!earliestEvent || timestampInMs < earliestEvent)) {
40+
if (session.segmentId === 0 && (!earliestEvent || timestampInMs < earliestEvent)) {
4141
replay.getContext().earliestEvent = timestampInMs;
4242
}
4343

4444
if (isCheckout) {
45-
await eventBuffer.clear();
45+
if (replay.recordingMode === 'error') {
46+
if (eventBuffer.lastCheckoutEventPos) {
47+
await eventBuffer.clear(eventBuffer.lastCheckoutEventPos);
48+
49+
if (!session.segmentId) {
50+
replay.getContext().earliestEvent = eventBuffer.pendingEvents[0].timestamp;
51+
}
52+
}
53+
54+
eventBuffer.lastCheckoutEventPos = eventBuffer.pendingLength;
55+
} else {
56+
await eventBuffer.clear();
57+
}
4658
}
4759

4860
return eventBuffer.addEvent(event);

packages/replay/test/integration/errorSampleRate.test.ts

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ describe('Integration | errorSampleRate', () => {
2626
beforeEach(async () => {
2727
({ mockRecord, domHandler, replay } = await resetSdkMock({
2828
replayOptions: {
29-
stickySession: true,
29+
stickySession: false,
3030
},
3131
sentryOptions: {
3232
replaysSessionSampleRate: 0.0,
@@ -321,11 +321,10 @@ describe('Integration | errorSampleRate', () => {
321321
});
322322
});
323323

324-
it('has correct timestamps when error occurs much later than initial pageload/checkout', async () => {
324+
it('keeps up to the last two checkout events', async () => {
325325
const ELAPSED = 60000;
326326
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
327327
mockRecord._emitter(TEST_EVENT);
328-
329328
// add a mock performance event
330329
replay.performanceEvents.push(PerformanceEntryResource());
331330

@@ -353,19 +352,17 @@ describe('Integration | errorSampleRate', () => {
353352

354353
expect(replay.session?.started).toBe(BASE_TIMESTAMP + ELAPSED + 20);
355354

356-
// Does not capture mouse click
355+
// Does capture everything from the previous checkout
357356
expect(replay).toHaveSentReplay({
358357
recordingPayloadHeader: { segment_id: 0 },
359358
replayEventPayload: expect.objectContaining({
360359
// Make sure the old performance event is thrown out
361360
replay_start_timestamp: (BASE_TIMESTAMP + ELAPSED + 20) / 1000,
362361
}),
363362
recordingData: JSON.stringify([
364-
{
365-
data: { isCheckout: true },
366-
timestamp: BASE_TIMESTAMP + ELAPSED + 20,
367-
type: 2,
368-
},
363+
{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP, type: 2 },
364+
{ data: {}, timestamp: BASE_TIMESTAMP, type: 3 },
365+
{ data: { isCheckout: true }, timestamp: BASE_TIMESTAMP + ELAPSED + 20, type: 2 },
369366
]),
370367
});
371368
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { addEvent } from '../../../src/util/addEvent';
2+
import { BASE_TIMESTAMP } from '../..';
3+
import { ReplayContainer } from '../../../src/replay';
4+
import { SESSION_IDLE_DURATION } from '../../../src/constants';
5+
import { createEventBuffer } from '../../../src/eventBuffer';
6+
import { clearSession } from '../../utils/clearSession';
7+
import { useFakeTimers } from '../../utils/use-fake-timers';
8+
import { setupReplayContainer } from '../../utils/setupReplayContainer';
9+
10+
useFakeTimers();
11+
12+
describe('Unit | util | addEvent', () => {
13+
it('clears queue after two checkouts in error mode', async function () {
14+
jest.setSystemTime(BASE_TIMESTAMP);
15+
16+
const replay = setupReplayContainer();
17+
replay.recordingMode = 'error';
18+
19+
await addEvent(replay, { data: {}, timestamp: BASE_TIMESTAMP, type: 2 }, false);
20+
await addEvent(replay, { data: {}, timestamp: BASE_TIMESTAMP + 10, type: 3 });
21+
await addEvent(replay, { data: {}, timestamp: BASE_TIMESTAMP + 100, type: 2 }, true);
22+
23+
expect(replay.getContext().earliestEvent).toEqual(BASE_TIMESTAMP);
24+
expect(replay.eventBuffer?.pendingEvents).toEqual([
25+
{ data: {}, timestamp: BASE_TIMESTAMP, type: 2 },
26+
{ data: {}, timestamp: BASE_TIMESTAMP + 10, type: 3 },
27+
{ data: {}, timestamp: BASE_TIMESTAMP + 100, type: 2 },
28+
]);
29+
30+
await addEvent(replay, { data: {}, timestamp: BASE_TIMESTAMP + 200, type: 2 }, true);
31+
32+
expect(replay.getContext().earliestEvent).toEqual(BASE_TIMESTAMP + 100);
33+
expect(replay.eventBuffer?.pendingEvents).toEqual([
34+
{ data: {}, timestamp: BASE_TIMESTAMP + 100, type: 2 },
35+
{ data: {}, timestamp: BASE_TIMESTAMP + 200, type: 2 },
36+
]);
37+
38+
await addEvent(replay, { data: {}, timestamp: BASE_TIMESTAMP + 250, type: 3 }, false);
39+
await addEvent(replay, { data: {}, timestamp: BASE_TIMESTAMP + 300, type: 2 }, true);
40+
41+
expect(replay.getContext().earliestEvent).toEqual(BASE_TIMESTAMP + 200);
42+
expect(replay.eventBuffer?.pendingEvents).toEqual([
43+
{ data: {}, timestamp: BASE_TIMESTAMP + 200, type: 2 },
44+
{ data: {}, timestamp: BASE_TIMESTAMP + 250, type: 3 },
45+
{ data: {}, timestamp: BASE_TIMESTAMP + 300, type: 2 },
46+
]);
47+
});
48+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { ReplayContainer } from '../../src/replay';
2+
import { clearSession } from './clearSession';
3+
import { SESSION_IDLE_DURATION } from '../../src/constants';
4+
import { createEventBuffer } from '../../src/eventBuffer';
5+
import { ReplayPluginOptions, RecordingOptions } from '../../src/types';
6+
7+
export function setupReplayContainer({
8+
options,
9+
recordingOptions,
10+
}: { options?: ReplayPluginOptions; recordingOptions?: RecordingOptions } = {}): ReplayContainer {
11+
const replay = new ReplayContainer({
12+
options: {
13+
flushMinDelay: 100,
14+
flushMaxDelay: 100,
15+
stickySession: false,
16+
sessionSampleRate: 0,
17+
errorSampleRate: 1,
18+
useCompression: false,
19+
maskAllText: true,
20+
blockAllMedia: true,
21+
_experiments: {},
22+
...options,
23+
},
24+
recordingOptions: {
25+
...recordingOptions,
26+
},
27+
});
28+
29+
clearSession(replay);
30+
replay['_setInitialState']();
31+
replay['_loadSession']({ expiry: SESSION_IDLE_DURATION });
32+
replay['_isEnabled'] = true;
33+
replay.eventBuffer = createEventBuffer({
34+
useCompression: false,
35+
});
36+
37+
return replay;
38+
}

0 commit comments

Comments
 (0)