Skip to content

Commit b83e7e1

Browse files
authored
feat(replay): Handle worker loading errors (#6827)
1 parent 5f26034 commit b83e7e1

File tree

6 files changed

+299
-92
lines changed

6 files changed

+299
-92
lines changed

packages/replay/jest.setup.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { TextEncoder } from 'util';
55

66
import type { ReplayContainer, Session } from './src/types';
77

8+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
89
(global as any).TextEncoder = TextEncoder;
910

1011
type MockTransport = jest.MockedFunction<Transport['send']>;

packages/replay/src/eventBuffer.ts

Lines changed: 107 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
// TODO: figure out member access types and remove the line above
33

4-
import { captureException } from '@sentry/core';
4+
import type { ReplayRecordingData } from '@sentry/types';
55
import { logger } from '@sentry/utils';
66

77
import type { AddEventResult, EventBuffer, RecordingEvent, WorkerRequest } from './types';
@@ -20,24 +20,90 @@ export function createEventBuffer({ useCompression }: CreateEventBufferParams):
2020
const workerBlob = new Blob([workerString]);
2121
const workerUrl = URL.createObjectURL(workerBlob);
2222

23-
try {
24-
__DEBUG_BUILD__ && logger.log('[Replay] Using compression worker');
25-
const worker = new Worker(workerUrl);
26-
if (worker) {
27-
return new EventBufferCompressionWorker(worker);
28-
} else {
29-
captureException(new Error('Unable to create compression worker'));
30-
}
31-
} catch {
32-
// catch and ignore, fallback to simple event buffer
33-
}
34-
__DEBUG_BUILD__ && logger.log('[Replay] Falling back to simple event buffer');
23+
__DEBUG_BUILD__ && logger.log('[Replay] Using compression worker');
24+
const worker = new Worker(workerUrl);
25+
return new EventBufferProxy(worker);
3526
}
3627

3728
__DEBUG_BUILD__ && logger.log('[Replay] Using simple buffer');
3829
return new EventBufferArray();
3930
}
4031

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+
41107
class EventBufferArray implements EventBuffer {
42108
private _events: RecordingEvent[];
43109

@@ -119,6 +185,34 @@ export class EventBufferCompressionWorker implements EventBuffer {
119185
return this._pendingEvents;
120186
}
121187

188+
/**
189+
* Ensure the worker is ready (or not).
190+
* This will either resolve when the worker is ready, or reject if an error occured.
191+
*/
192+
public ensureReady(): Promise<void> {
193+
return new Promise((resolve, reject) => {
194+
this._worker.addEventListener(
195+
'message',
196+
({ data }: MessageEvent) => {
197+
if (data.success) {
198+
resolve();
199+
} else {
200+
reject();
201+
}
202+
},
203+
{ once: true },
204+
);
205+
206+
this._worker.addEventListener(
207+
'error',
208+
error => {
209+
reject(error);
210+
},
211+
{ once: true },
212+
);
213+
});
214+
}
215+
122216
/**
123217
* Destroy the event buffer.
124218
*/

packages/replay/src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ export interface EventBuffer {
225225
/**
226226
* Add an event to the event buffer.
227227
*
228-
* Returns true if event was successfully added.
228+
* Returns a promise that resolves if the event was successfully added, else rejects.
229229
*/
230230
addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult>;
231231

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.

0 commit comments

Comments
 (0)