Skip to content

Commit 34bb403

Browse files
authored
feat(replay): Stop replay when event buffer exceeds max. size (#8315)
When the buffer exceeds ~20MB, stop the replay. Closes #7657 Closes getsentry/team-replay#94
1 parent e6ea537 commit 34bb403

File tree

8 files changed

+192
-7
lines changed

8 files changed

+192
-7
lines changed

packages/replay/src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,6 @@ export const SLOW_CLICK_THRESHOLD = 3_000;
4444
export const SLOW_CLICK_SCROLL_TIMEOUT = 300;
4545
/* Clicks in this time period are considered e.g. double/triple clicks. */
4646
export const MULTI_CLICK_TIMEOUT = 1_000;
47+
48+
/** When encountering a total segment size exceeding this size, stop the replay (as we cannot properly ingest it). */
49+
export const REPLAY_MAX_EVENT_BUFFER_SIZE = 20_000_000; // ~20MB

packages/replay/src/eventBuffer/EventBufferArray.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants';
12
import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types';
23
import { timestampToMs } from '../util/timestampToMs';
4+
import { EventBufferSizeExceededError } from '.';
35

46
/**
57
* A basic event buffer that does not do any compression.
@@ -8,6 +10,7 @@ import { timestampToMs } from '../util/timestampToMs';
810
export class EventBufferArray implements EventBuffer {
911
/** All the events that are buffered to be sent. */
1012
public events: RecordingEvent[];
13+
private _totalSize = 0;
1114

1215
public constructor() {
1316
this.events = [];
@@ -30,6 +33,12 @@ export class EventBufferArray implements EventBuffer {
3033

3134
/** @inheritdoc */
3235
public async addEvent(event: RecordingEvent): Promise<AddEventResult> {
36+
const eventSize = JSON.stringify(event).length;
37+
this._totalSize += eventSize;
38+
if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
39+
throw new EventBufferSizeExceededError();
40+
}
41+
3342
this.events.push(event);
3443
}
3544

@@ -40,14 +49,15 @@ export class EventBufferArray implements EventBuffer {
4049
// events member so that we do not lose new events while uploading
4150
// attachment.
4251
const eventsRet = this.events;
43-
this.events = [];
52+
this.clear();
4453
resolve(JSON.stringify(eventsRet));
4554
});
4655
}
4756

4857
/** @inheritdoc */
4958
public clear(): void {
5059
this.events = [];
60+
this._totalSize = 0;
5161
}
5262

5363
/** @inheritdoc */

packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import type { ReplayRecordingData } from '@sentry/types';
22

3+
import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants';
34
import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } from '../types';
45
import { timestampToMs } from '../util/timestampToMs';
6+
import { EventBufferSizeExceededError } from '.';
57
import { WorkerHandler } from './WorkerHandler';
68

79
/**
@@ -11,6 +13,7 @@ import { WorkerHandler } from './WorkerHandler';
1113
export class EventBufferCompressionWorker implements EventBuffer {
1214
private _worker: WorkerHandler;
1315
private _earliestTimestamp: number | null;
16+
private _totalSize = 0;
1417

1518
public constructor(worker: Worker) {
1619
this._worker = new WorkerHandler(worker);
@@ -53,7 +56,14 @@ export class EventBufferCompressionWorker implements EventBuffer {
5356
this._earliestTimestamp = timestamp;
5457
}
5558

56-
return this._sendEventToWorker(event);
59+
const data = JSON.stringify(event);
60+
this._totalSize += data.length;
61+
62+
if (this._totalSize > REPLAY_MAX_EVENT_BUFFER_SIZE) {
63+
return Promise.reject(new EventBufferSizeExceededError());
64+
}
65+
66+
return this._sendEventToWorker(data);
5767
}
5868

5969
/**
@@ -66,6 +76,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
6676
/** @inheritdoc */
6777
public clear(): void {
6878
this._earliestTimestamp = null;
79+
this._totalSize = 0;
6980
// We do not wait on this, as we assume the order of messages is consistent for the worker
7081
void this._worker.postMessage('clear');
7182
}
@@ -78,8 +89,8 @@ export class EventBufferCompressionWorker implements EventBuffer {
7889
/**
7990
* Send the event to the worker.
8091
*/
81-
private _sendEventToWorker(event: RecordingEvent): Promise<AddEventResult> {
82-
return this._worker.postMessage<void>('addEvent', JSON.stringify(event));
92+
private _sendEventToWorker(data: string): Promise<AddEventResult> {
93+
return this._worker.postMessage<void>('addEvent', data);
8394
}
8495

8596
/**
@@ -89,6 +100,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
89100
const response = await this._worker.postMessage<Uint8Array>('finish');
90101

91102
this._earliestTimestamp = null;
103+
this._totalSize = 0;
92104

93105
return response;
94106
}

packages/replay/src/eventBuffer/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { getWorkerURL } from '@sentry-internal/replay-worker';
22
import { logger } from '@sentry/utils';
33

4+
import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../constants';
45
import type { EventBuffer } from '../types';
56
import { EventBufferArray } from './EventBufferArray';
67
import { EventBufferProxy } from './EventBufferProxy';
@@ -30,3 +31,10 @@ export function createEventBuffer({ useCompression }: CreateEventBufferParams):
3031
__DEBUG_BUILD__ && logger.log('[Replay] Using simple buffer');
3132
return new EventBufferArray();
3233
}
34+
35+
/** This error indicates that the event buffer size exceeded the limit.. */
36+
export class EventBufferSizeExceededError extends Error {
37+
public constructor() {
38+
super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`);
39+
}
40+
}

packages/replay/src/util/addEvent.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { EventType } from '@sentry-internal/rrweb';
22
import { getCurrentHub } from '@sentry/core';
33
import { logger } from '@sentry/utils';
44

5+
import { EventBufferSizeExceededError } from '../eventBuffer';
56
import type { AddEventResult, RecordingEvent, ReplayContainer, ReplayFrameEvent } from '../types';
67
import { timestampToMs } from './timestampToMs';
78

@@ -56,8 +57,10 @@ export async function addEvent(
5657

5758
return await replay.eventBuffer.addEvent(eventAfterPossibleCallback);
5859
} catch (error) {
60+
const reason = error && error instanceof EventBufferSizeExceededError ? 'addEventSizeExceeded' : 'addEvent';
61+
5962
__DEBUG_BUILD__ && logger.error(error);
60-
await replay.stop('addEvent');
63+
await replay.stop(reason);
6164

6265
const client = getCurrentHub().getClient();
6366

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

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { createEventBuffer } from './../../../src/eventBuffer';
1+
import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../../../src/constants';
2+
import { createEventBuffer, EventBufferSizeExceededError } from './../../../src/eventBuffer';
23
import { BASE_TIMESTAMP } from './../../index';
34

45
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
@@ -44,4 +45,56 @@ describe('Unit | eventBuffer | EventBufferArray', () => {
4445
expect(result1).toEqual(JSON.stringify([TEST_EVENT]));
4546
expect(result2).toEqual(JSON.stringify([]));
4647
});
48+
49+
describe('size limit', () => {
50+
it('rejects if size exceeds limit', async function () {
51+
const buffer = createEventBuffer({ useCompression: false });
52+
53+
const largeEvent = {
54+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
55+
timestamp: BASE_TIMESTAMP,
56+
type: 3,
57+
};
58+
59+
await buffer.addEvent(largeEvent);
60+
await buffer.addEvent(largeEvent);
61+
62+
// Now it should error
63+
await expect(() => buffer.addEvent(largeEvent)).rejects.toThrowError(EventBufferSizeExceededError);
64+
});
65+
66+
it('resets size limit on clear', async function () {
67+
const buffer = createEventBuffer({ useCompression: false });
68+
69+
const largeEvent = {
70+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
71+
timestamp: BASE_TIMESTAMP,
72+
type: 3,
73+
};
74+
75+
await buffer.addEvent(largeEvent);
76+
await buffer.addEvent(largeEvent);
77+
78+
await buffer.clear();
79+
80+
await buffer.addEvent(largeEvent);
81+
});
82+
83+
it('resets size limit on finish', async function () {
84+
const buffer = createEventBuffer({ useCompression: false });
85+
86+
const largeEvent = {
87+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
88+
timestamp: BASE_TIMESTAMP,
89+
type: 3,
90+
};
91+
92+
await buffer.addEvent(largeEvent);
93+
await buffer.addEvent(largeEvent);
94+
95+
await buffer.finish();
96+
97+
await buffer.addEvent(largeEvent);
98+
});
99+
});
47100
});

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

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import 'jsdom-worker';
33
import pako from 'pako';
44

55
import { BASE_TIMESTAMP } from '../..';
6+
import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../../../src/constants';
67
import { EventBufferProxy } from '../../../src/eventBuffer/EventBufferProxy';
7-
import { createEventBuffer } from './../../../src/eventBuffer';
8+
import { createEventBuffer, EventBufferSizeExceededError } from './../../../src/eventBuffer';
89

910
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
1011

@@ -146,4 +147,71 @@ describe('Unit | eventBuffer | EventBufferCompressionWorker', () => {
146147

147148
await expect(() => buffer.addEvent({ data: { o: 3 }, timestamp: BASE_TIMESTAMP, type: 3 })).rejects.toBeDefined();
148149
});
150+
151+
describe('size limit', () => {
152+
it('rejects if size exceeds limit', async function () {
153+
const buffer = createEventBuffer({
154+
useCompression: true,
155+
}) as EventBufferProxy;
156+
157+
expect(buffer).toBeInstanceOf(EventBufferProxy);
158+
await buffer.ensureWorkerIsLoaded();
159+
160+
const largeEvent = {
161+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
162+
timestamp: BASE_TIMESTAMP,
163+
type: 3,
164+
};
165+
166+
await buffer.addEvent(largeEvent);
167+
await buffer.addEvent(largeEvent);
168+
169+
// Now it should error
170+
await expect(() => buffer.addEvent(largeEvent)).rejects.toThrowError(EventBufferSizeExceededError);
171+
});
172+
173+
it('resets size limit on clear', async function () {
174+
const buffer = createEventBuffer({
175+
useCompression: true,
176+
}) as EventBufferProxy;
177+
178+
expect(buffer).toBeInstanceOf(EventBufferProxy);
179+
await buffer.ensureWorkerIsLoaded();
180+
181+
const largeEvent = {
182+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
183+
timestamp: BASE_TIMESTAMP,
184+
type: 3,
185+
};
186+
187+
await buffer.addEvent(largeEvent);
188+
await buffer.addEvent(largeEvent);
189+
190+
await buffer.clear();
191+
192+
await buffer.addEvent(largeEvent);
193+
});
194+
195+
it('resets size limit on finish', async function () {
196+
const buffer = createEventBuffer({
197+
useCompression: true,
198+
}) as EventBufferProxy;
199+
200+
expect(buffer).toBeInstanceOf(EventBufferProxy);
201+
await buffer.ensureWorkerIsLoaded();
202+
203+
const largeEvent = {
204+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
205+
timestamp: BASE_TIMESTAMP,
206+
type: 3,
207+
};
208+
209+
await buffer.addEvent(largeEvent);
210+
await buffer.addEvent(largeEvent);
211+
212+
await buffer.finish();
213+
214+
await buffer.addEvent(largeEvent);
215+
});
216+
});
149217
});

packages/replay/test/unit/util/addEvent.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'jsdom-worker';
22

33
import { BASE_TIMESTAMP } from '../..';
4+
import { REPLAY_MAX_EVENT_BUFFER_SIZE } from '../../../src/constants';
45
import type { EventBufferProxy } from '../../../src/eventBuffer/EventBufferProxy';
56
import { addEvent } from '../../../src/util/addEvent';
67
import { setupReplayContainer } from '../../utils/setupReplayContainer';
@@ -29,4 +30,31 @@ describe('Unit | util | addEvent', () => {
2930

3031
expect(replay.isEnabled()).toEqual(false);
3132
});
33+
34+
it('stops when exceeding buffer size limit', async function () {
35+
jest.setSystemTime(BASE_TIMESTAMP);
36+
37+
const replay = setupReplayContainer({
38+
options: {
39+
useCompression: true,
40+
},
41+
});
42+
43+
const largeEvent = {
44+
data: { a: 'a'.repeat(REPLAY_MAX_EVENT_BUFFER_SIZE / 3) },
45+
timestamp: BASE_TIMESTAMP,
46+
type: 3,
47+
};
48+
49+
await (replay.eventBuffer as EventBufferProxy).ensureWorkerIsLoaded();
50+
51+
await addEvent(replay, largeEvent);
52+
await addEvent(replay, largeEvent);
53+
54+
expect(replay.isEnabled()).toEqual(true);
55+
56+
await addEvent(replay, largeEvent);
57+
58+
expect(replay.isEnabled()).toEqual(false);
59+
});
3260
});

0 commit comments

Comments
 (0)