Skip to content

Commit ef83db6

Browse files
authored
feat(replay): Track pending events in EventBuffer (#6699)
* Track raw pending events in `EventBuffer`. This can be helpful in the case where page is reloaded before the worker has a chance to close the compression stream. * Change `EventBuffer.length` to `EventBuffer.pendingLength` to better reflect what it is. In the case of compression worker, it is async, so recent events added to the buffer are not necessarily present in the workers compression stream.
1 parent 770a33d commit ef83db6

File tree

7 files changed

+60
-20
lines changed

7 files changed

+60
-20
lines changed

packages/replay/src/eventBuffer.ts

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,18 @@ class EventBufferArray implements EventBuffer {
4545
this._events = [];
4646
}
4747

48-
public get length(): number {
48+
public get pendingLength(): number {
4949
return this._events.length;
5050
}
5151

52+
/**
53+
* Returns the raw events that are buffered. In `EventBufferArray`, this is the
54+
* same as `this._events`.
55+
*/
56+
public get pendingEvents(): RecordingEvent[] {
57+
return this._events;
58+
}
59+
5260
public destroy(): void {
5361
this._events = [];
5462
}
@@ -80,6 +88,13 @@ class EventBufferArray implements EventBuffer {
8088
* Exported only for testing.
8189
*/
8290
export class EventBufferCompressionWorker implements EventBuffer {
91+
/**
92+
* Keeps track of the list of events since the last flush that have not been compressed.
93+
* For example, page is reloaded and a flush attempt is made, but
94+
* `finish()` (and thus the flush), does not complete.
95+
*/
96+
public _pendingEvents: RecordingEvent[] = [];
97+
8398
private _worker: null | Worker;
8499
private _eventBufferItemLength: number = 0;
85100
private _id: number = 0;
@@ -89,13 +104,21 @@ export class EventBufferCompressionWorker implements EventBuffer {
89104
}
90105

91106
/**
92-
* Note that this may not reflect what is actually in the event buffer. This
93-
* is only a local count of the buffer size since `addEvent` is async.
107+
* The number of raw events that are buffered. This may not be the same as
108+
* the number of events that have been compresed in the worker because
109+
* `addEvent` is async.
94110
*/
95-
public get length(): number {
111+
public get pendingLength(): number {
96112
return this._eventBufferItemLength;
97113
}
98114

115+
/**
116+
* Returns a list of the raw recording events that are being compressed.
117+
*/
118+
public get pendingEvents(): RecordingEvent[] {
119+
return this._pendingEvents;
120+
}
121+
99122
/**
100123
* Destroy the event buffer.
101124
*/
@@ -121,6 +144,11 @@ export class EventBufferCompressionWorker implements EventBuffer {
121144
});
122145
}
123146

147+
// Don't store checkout events in `_pendingEvents` because they are too large
148+
if (!isCheckout) {
149+
this._pendingEvents.push(event);
150+
}
151+
124152
return this._sendEventToWorker(event);
125153
}
126154

@@ -202,6 +230,10 @@ export class EventBufferCompressionWorker implements EventBuffer {
202230
// XXX: See note in `get length()`
203231
this._eventBufferItemLength = 0;
204232

233+
await promise;
234+
235+
this._pendingEvents = [];
236+
205237
return promise;
206238
}
207239

packages/replay/src/replay.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -794,7 +794,7 @@ export class ReplayContainer implements ReplayContainerInterface {
794794

795795
await this.addPerformanceEntries();
796796

797-
if (!this.eventBuffer?.length) {
797+
if (!this.eventBuffer?.pendingLength) {
798798
return;
799799
}
800800

packages/replay/src/types.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,15 @@ export interface Session {
211211
}
212212

213213
export interface EventBuffer {
214-
readonly length: number;
214+
/**
215+
* The number of raw events that are buffered
216+
*/
217+
readonly pendingLength: number;
218+
219+
/**
220+
* The raw events that are buffered.
221+
*/
222+
readonly pendingEvents: RecordingEvent[];
215223

216224
/**
217225
* Destroy the event buffer.
@@ -226,7 +234,7 @@ export interface EventBuffer {
226234
addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult>;
227235

228236
/**
229-
* Clears and returns the contents and the buffer.
237+
* Clears and returns the contents of the buffer.
230238
*/
231239
finish(): Promise<ReplayRecordingData>;
232240
}

packages/replay/src/worker/worker.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/replay/test/integration/sendReplayEvent.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ describe('Integration | sendReplayEvent', () => {
102102
expect(replay.session?.segmentId).toBe(1);
103103

104104
// events array should be empty
105-
expect(replay.eventBuffer?.length).toBe(0);
105+
expect(replay.eventBuffer?.pendingLength).toBe(0);
106106
});
107107

108108
it('update last activity when user clicks mouse', async () => {
@@ -141,7 +141,7 @@ describe('Integration | sendReplayEvent', () => {
141141
expect(replay.session?.segmentId).toBe(1);
142142

143143
// events array should be empty
144-
expect(replay.eventBuffer?.length).toBe(0);
144+
expect(replay.eventBuffer?.pendingLength).toBe(0);
145145
});
146146

147147
it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => {
@@ -169,7 +169,7 @@ describe('Integration | sendReplayEvent', () => {
169169
expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP);
170170
expect(replay.session?.segmentId).toBe(1);
171171
// events array should be empty
172-
expect(replay.eventBuffer?.length).toBe(0);
172+
expect(replay.eventBuffer?.pendingLength).toBe(0);
173173

174174
// Let's make sure it continues to work
175175
mockTransportSend.mockClear();
@@ -214,7 +214,7 @@ describe('Integration | sendReplayEvent', () => {
214214
// Session's last activity should not be updated
215215
expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP);
216216
// events array should be empty
217-
expect(replay.eventBuffer?.length).toBe(0);
217+
expect(replay.eventBuffer?.pendingLength).toBe(0);
218218
});
219219

220220
it('uploads a replay event when document becomes hidden', async () => {
@@ -242,7 +242,7 @@ describe('Integration | sendReplayEvent', () => {
242242
// visibilitystate as user being active
243243
expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP);
244244
// events array should be empty
245-
expect(replay.eventBuffer?.length).toBe(0);
245+
expect(replay.eventBuffer?.pendingLength).toBe(0);
246246
});
247247

248248
it('uploads a replay event if 5 seconds have elapsed since the last replay event occurred', async () => {
@@ -261,7 +261,7 @@ describe('Integration | sendReplayEvent', () => {
261261
expect(replay.session?.segmentId).toBe(1);
262262

263263
// events array should be empty
264-
expect(replay.eventBuffer?.length).toBe(0);
264+
expect(replay.eventBuffer?.pendingLength).toBe(0);
265265
});
266266

267267
it('uploads a replay event if 15 seconds have elapsed since the last replay upload', async () => {
@@ -290,7 +290,7 @@ describe('Integration | sendReplayEvent', () => {
290290
expect(replay.session?.lastActivity).toBe(BASE_TIMESTAMP);
291291
expect(replay.session?.segmentId).toBe(1);
292292
// events array should be empty
293-
expect(replay.eventBuffer?.length).toBe(0);
293+
expect(replay.eventBuffer?.pendingLength).toBe(0);
294294

295295
// Let's make sure it continues to work
296296
mockTransportSend.mockClear();

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -128,17 +128,17 @@ describe('Integration | stop', () => {
128128

129129
it('does not buffer events when stopped', async function () {
130130
WINDOW.dispatchEvent(new Event('blur'));
131-
expect(replay.eventBuffer?.length).toBe(1);
131+
expect(replay.eventBuffer?.pendingLength).toBe(1);
132132

133133
// stop replays
134134
integration.stop();
135135

136-
expect(replay.eventBuffer?.length).toBe(undefined);
136+
expect(replay.eventBuffer?.pendingLength).toBe(undefined);
137137

138138
WINDOW.dispatchEvent(new Event('blur'));
139139
await new Promise(process.nextTick);
140140

141-
expect(replay.eventBuffer?.length).toBe(undefined);
141+
expect(replay.eventBuffer?.pendingLength).toBe(undefined);
142142
expect(replay).not.toHaveLastSentReplay();
143143
});
144144

packages/replay/test/unit/eventBuffer.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ describe('Unit | eventBuffer', () => {
5353
}) as EventBufferCompressionWorker;
5454

5555
buffer.addEvent(TEST_EVENT);
56-
// @ts-ignore make sure it handles invalid data
57-
buffer.addEvent(undefined);
5856
buffer.addEvent(TEST_EVENT);
5957

58+
expect(buffer.pendingEvents).toEqual([TEST_EVENT, TEST_EVENT]);
59+
6060
const result = await buffer.finish();
6161
const restored = pako.inflate(result, { to: 'string' });
6262

0 commit comments

Comments
 (0)