Skip to content

Commit 2964546

Browse files
committed
ref(replay): Refactor eventBuffer into separate files
1 parent d737b15 commit 2964546

File tree

5 files changed

+174
-157
lines changed

5 files changed

+174
-157
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
2+
3+
/**
4+
* A basic event buffer that does not do any compression.
5+
* Used as fallback if the compression worker cannot be loaded or is disabled.
6+
*/
7+
export class EventBufferArray implements EventBuffer {
8+
private _events: RecordingEvent[];
9+
10+
public constructor() {
11+
this._events = [];
12+
}
13+
14+
/** @inheritdoc */
15+
public get pendingLength(): number {
16+
return this._events.length;
17+
}
18+
19+
/**
20+
* Returns the raw events that are buffered. In `EventBufferArray`, this is the
21+
* same as `this._events`.
22+
*/
23+
public get pendingEvents(): RecordingEvent[] {
24+
return this._events;
25+
}
26+
27+
/** @inheritdoc */
28+
public destroy(): void {
29+
this._events = [];
30+
}
31+
32+
/** @inheritdoc */
33+
public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
34+
if (isCheckout) {
35+
this._events = [event];
36+
return;
37+
}
38+
39+
this._events.push(event);
40+
return;
41+
}
42+
43+
/** @inheritdoc */
44+
public finish(): Promise<string> {
45+
return new Promise<string>(resolve => {
46+
// Make a copy of the events array reference and immediately clear the
47+
// events member so that we do not lose new events while uploading
48+
// attachment.
49+
const eventsRet = this._events;
50+
this._events = [];
51+
resolve(JSON.stringify(eventsRet));
52+
});
53+
}
54+
}

packages/replay/src/eventBuffer.ts renamed to packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts

Lines changed: 9 additions & 156 deletions
Original file line numberDiff line numberDiff line change
@@ -1,153 +1,6 @@
1-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
2-
// TODO: figure out member access types and remove the line above
3-
4-
import type { ReplayRecordingData } from '@sentry/types';
51
import { logger } from '@sentry/utils';
62

7-
import type { AddEventResult, EventBuffer, RecordingEvent, WorkerRequest } from './types';
8-
import workerString from './worker/worker.js';
9-
10-
interface CreateEventBufferParams {
11-
useCompression: boolean;
12-
}
13-
14-
/**
15-
* Create an event buffer for replays.
16-
*/
17-
export function createEventBuffer({ useCompression }: CreateEventBufferParams): EventBuffer {
18-
// eslint-disable-next-line no-restricted-globals
19-
if (useCompression && window.Worker) {
20-
const workerBlob = new Blob([workerString]);
21-
const workerUrl = URL.createObjectURL(workerBlob);
22-
23-
__DEBUG_BUILD__ && logger.log('[Replay] Using compression worker');
24-
const worker = new Worker(workerUrl);
25-
return new EventBufferProxy(worker);
26-
}
27-
28-
__DEBUG_BUILD__ && logger.log('[Replay] Using simple buffer');
29-
return new EventBufferArray();
30-
}
31-
32-
/**
33-
* This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
34-
* This can happen e.g. if the worker cannot be loaded.
35-
* Exported only for testing.
36-
*/
37-
export class EventBufferProxy implements EventBuffer {
38-
private _fallback: EventBufferArray;
39-
private _compression: EventBufferCompressionWorker;
40-
private _used: EventBuffer;
41-
42-
public constructor(worker: Worker) {
43-
this._fallback = new EventBufferArray();
44-
this._compression = new EventBufferCompressionWorker(worker);
45-
this._used = this._fallback;
46-
47-
void this._ensureWorkerIsLoaded();
48-
}
49-
50-
/** @inheritDoc */
51-
public get pendingLength(): number {
52-
return this._used.pendingLength;
53-
}
54-
55-
/** @inheritDoc */
56-
public get pendingEvents(): RecordingEvent[] {
57-
return this._used.pendingEvents;
58-
}
59-
60-
/** @inheritDoc */
61-
public destroy(): void {
62-
this._fallback.destroy();
63-
this._compression.destroy();
64-
}
65-
66-
/**
67-
* Add an event to the event buffer.
68-
*
69-
* Returns true if event was successfully added.
70-
*/
71-
public addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
72-
return this._used.addEvent(event, isCheckout);
73-
}
74-
75-
/** @inheritDoc */
76-
public finish(): Promise<ReplayRecordingData> {
77-
return this._used.finish();
78-
}
79-
80-
/** Ensure the worker has loaded. */
81-
private async _ensureWorkerIsLoaded(): Promise<void> {
82-
try {
83-
await this._compression.ensureReady();
84-
} catch (error) {
85-
// If the worker fails to load, we fall back to the simple buffer.
86-
// Nothing more to do from our side here
87-
__DEBUG_BUILD__ && logger.log('[Replay] Failed to load the compression worker, falling back to simple buffer');
88-
return;
89-
}
90-
91-
// Compression worker is ready, we can use it
92-
// Now we need to switch over the array buffer to the compression worker
93-
const addEventPromises: Promise<void>[] = [];
94-
for (const event of this._fallback.pendingEvents) {
95-
addEventPromises.push(this._compression.addEvent(event));
96-
}
97-
98-
// We switch over to the compression buffer immediately - any further events will be added
99-
// after the previously buffered ones
100-
this._used = this._compression;
101-
102-
// Wait for original events to be re-added before resolving
103-
await Promise.all(addEventPromises);
104-
}
105-
}
106-
107-
class EventBufferArray implements EventBuffer {
108-
private _events: RecordingEvent[];
109-
110-
public constructor() {
111-
this._events = [];
112-
}
113-
114-
public get pendingLength(): number {
115-
return this._events.length;
116-
}
117-
118-
/**
119-
* Returns the raw events that are buffered. In `EventBufferArray`, this is the
120-
* same as `this._events`.
121-
*/
122-
public get pendingEvents(): RecordingEvent[] {
123-
return this._events;
124-
}
125-
126-
public destroy(): void {
127-
this._events = [];
128-
}
129-
130-
public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
131-
if (isCheckout) {
132-
this._events = [event];
133-
return;
134-
}
135-
136-
this._events.push(event);
137-
return;
138-
}
139-
140-
public finish(): Promise<string> {
141-
return new Promise<string>(resolve => {
142-
// Make a copy of the events array reference and immediately clear the
143-
// events member so that we do not lose new events while uploading
144-
// attachment.
145-
const eventsRet = this._events;
146-
this._events = [];
147-
resolve(JSON.stringify(eventsRet));
148-
});
149-
}
150-
}
3+
import type { AddEventResult, EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from '../types';
1514

1525
/**
1536
* Event buffer that uses a web worker to compress events.
@@ -194,7 +47,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
19447
this._worker.addEventListener(
19548
'message',
19649
({ data }: MessageEvent) => {
197-
if (data.success) {
50+
if ((data as WorkerResponse).success) {
19851
resolve();
19952
} else {
20053
reject();
@@ -257,30 +110,30 @@ export class EventBufferCompressionWorker implements EventBuffer {
257110
*/
258111
private _postMessage<T>({ id, method, args }: WorkerRequest): Promise<T> {
259112
return new Promise((resolve, reject) => {
260-
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
261-
const listener = ({ data }: MessageEvent) => {
262-
if (data.method !== method) {
113+
const listener = ({ data }: MessageEvent): void => {
114+
const response = data as WorkerResponse;
115+
if (response.method !== method) {
263116
return;
264117
}
265118

266119
// There can be multiple listeners for a single method, the id ensures
267120
// that the response matches the caller.
268-
if (data.id !== id) {
121+
if (response.id !== id) {
269122
return;
270123
}
271124

272125
// At this point, we'll always want to remove listener regardless of result status
273126
this._worker.removeEventListener('message', listener);
274127

275-
if (!data.success) {
128+
if (!response.success) {
276129
// TODO: Do some error handling, not sure what
277-
__DEBUG_BUILD__ && logger.error('[Replay]', data.response);
130+
__DEBUG_BUILD__ && logger.error('[Replay]', response.response);
278131

279132
reject(new Error('Error in compression worker'));
280133
return;
281134
}
282135

283-
resolve(data.response);
136+
resolve(response.response as T);
284137
};
285138

286139
let stringifiedArgs;
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { ReplayRecordingData } from '@sentry/types';
2+
import { logger } from '@sentry/utils';
3+
4+
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
5+
import { EventBufferArray } from './EventBufferArray';
6+
import { EventBufferCompressionWorker } from './EventBufferCompressionWorker';
7+
8+
/**
9+
* This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
10+
* This can happen e.g. if the worker cannot be loaded.
11+
* Exported only for testing.
12+
*/
13+
export class EventBufferProxy implements EventBuffer {
14+
private _fallback: EventBufferArray;
15+
private _compression: EventBufferCompressionWorker;
16+
private _used: EventBuffer;
17+
18+
public constructor(worker: Worker) {
19+
this._fallback = new EventBufferArray();
20+
this._compression = new EventBufferCompressionWorker(worker);
21+
this._used = this._fallback;
22+
23+
void this._ensureWorkerIsLoaded();
24+
}
25+
26+
/** @inheritDoc */
27+
public get pendingLength(): number {
28+
return this._used.pendingLength;
29+
}
30+
31+
/** @inheritDoc */
32+
public get pendingEvents(): RecordingEvent[] {
33+
return this._used.pendingEvents;
34+
}
35+
36+
/** @inheritDoc */
37+
public destroy(): void {
38+
this._fallback.destroy();
39+
this._compression.destroy();
40+
}
41+
42+
/**
43+
* Add an event to the event buffer.
44+
*
45+
* Returns true if event was successfully added.
46+
*/
47+
public addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
48+
return this._used.addEvent(event, isCheckout);
49+
}
50+
51+
/** @inheritDoc */
52+
public finish(): Promise<ReplayRecordingData> {
53+
return this._used.finish();
54+
}
55+
56+
/** Ensure the worker has loaded. */
57+
private async _ensureWorkerIsLoaded(): Promise<void> {
58+
try {
59+
await this._compression.ensureReady();
60+
} catch (error) {
61+
// If the worker fails to load, we fall back to the simple buffer.
62+
// Nothing more to do from our side here
63+
__DEBUG_BUILD__ && logger.log('[Replay] Failed to load the compression worker, falling back to simple buffer');
64+
return;
65+
}
66+
67+
// Compression worker is ready, we can use it
68+
// Now we need to switch over the array buffer to the compression worker
69+
const addEventPromises: Promise<void>[] = [];
70+
for (const event of this._fallback.pendingEvents) {
71+
addEventPromises.push(this._compression.addEvent(event));
72+
}
73+
74+
// We switch over to the compression buffer immediately - any further events will be added
75+
// after the previously buffered ones
76+
this._used = this._compression;
77+
78+
// Wait for original events to be re-added before resolving
79+
await Promise.all(addEventPromises);
80+
}
81+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { logger } from '@sentry/utils';
2+
3+
import type { EventBuffer } from '../types';
4+
import workerString from '../worker/worker.js';
5+
import { EventBufferArray } from './EventBufferArray';
6+
import { EventBufferProxy } from './EventBufferProxy';
7+
8+
interface CreateEventBufferParams {
9+
useCompression: boolean;
10+
}
11+
12+
/**
13+
* Create an event buffer for replays.
14+
*/
15+
export function createEventBuffer({ useCompression }: CreateEventBufferParams): EventBuffer {
16+
// eslint-disable-next-line no-restricted-globals
17+
if (useCompression && window.Worker) {
18+
const workerBlob = new Blob([workerString]);
19+
const workerUrl = URL.createObjectURL(workerBlob);
20+
21+
__DEBUG_BUILD__ && logger.log('[Replay] Using compression worker');
22+
const worker = new Worker(workerUrl);
23+
return new EventBufferProxy(worker);
24+
}
25+
26+
__DEBUG_BUILD__ && logger.log('[Replay] Using simple buffer');
27+
return new EventBufferArray();
28+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@ import 'jsdom-worker';
22

33
import pako from 'pako';
44

5-
import { createEventBuffer, EventBufferProxy } from './../../src/eventBuffer';
5+
import { createEventBuffer } from './../../src/eventBuffer';
66
import { BASE_TIMESTAMP } from './../index';
7+
import { EventBufferProxy } from '../../src/eventBuffer/EventBufferProxy';
78

89
const TEST_EVENT = { data: {}, timestamp: BASE_TIMESTAMP, type: 3 };
910
describe('Unit | eventBuffer', () => {

0 commit comments

Comments
 (0)