Skip to content

Commit

Permalink
feat(replay): Handle worker loading errors (#6827)
Browse files Browse the repository at this point in the history
  • Loading branch information
mydea committed Jan 19, 2023
1 parent 5f26034 commit b83e7e1
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 92 deletions.
1 change: 1 addition & 0 deletions packages/replay/jest.setup.ts
Expand Up @@ -5,6 +5,7 @@ import { TextEncoder } from 'util';

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

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

type MockTransport = jest.MockedFunction<Transport['send']>;
Expand Down
120 changes: 107 additions & 13 deletions packages/replay/src/eventBuffer.ts
@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
// TODO: figure out member access types and remove the line above

import { captureException } from '@sentry/core';
import type { ReplayRecordingData } from '@sentry/types';
import { logger } from '@sentry/utils';

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

try {
__DEBUG_BUILD__ && logger.log('[Replay] Using compression worker');
const worker = new Worker(workerUrl);
if (worker) {
return new EventBufferCompressionWorker(worker);
} else {
captureException(new Error('Unable to create compression worker'));
}
} catch {
// catch and ignore, fallback to simple event buffer
}
__DEBUG_BUILD__ && logger.log('[Replay] Falling back to simple event buffer');
__DEBUG_BUILD__ && logger.log('[Replay] Using compression worker');
const worker = new Worker(workerUrl);
return new EventBufferProxy(worker);
}

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

/**
* This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
* This can happen e.g. if the worker cannot be loaded.
* Exported only for testing.
*/
export class EventBufferProxy implements EventBuffer {
private _fallback: EventBufferArray;
private _compression: EventBufferCompressionWorker;
private _used: EventBuffer;

public constructor(worker: Worker) {
this._fallback = new EventBufferArray();
this._compression = new EventBufferCompressionWorker(worker);
this._used = this._fallback;

void this._ensureWorkerIsLoaded();
}

/** @inheritDoc */
public get pendingLength(): number {
return this._used.pendingLength;
}

/** @inheritDoc */
public get pendingEvents(): RecordingEvent[] {
return this._used.pendingEvents;
}

/** @inheritDoc */
public destroy(): void {
this._fallback.destroy();
this._compression.destroy();
}

/**
* Add an event to the event buffer.
*
* Returns true if event was successfully added.
*/
public addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
return this._used.addEvent(event, isCheckout);
}

/** @inheritDoc */
public finish(): Promise<ReplayRecordingData> {
return this._used.finish();
}

/** Ensure the worker has loaded. */
private async _ensureWorkerIsLoaded(): Promise<void> {
try {
await this._compression.ensureReady();
} catch (error) {
// If the worker fails to load, we fall back to the simple buffer.
// Nothing more to do from our side here
__DEBUG_BUILD__ && logger.log('[Replay] Failed to load the compression worker, falling back to simple buffer');
return;
}

// Compression worker is ready, we can use it
// Now we need to switch over the array buffer to the compression worker
const addEventPromises: Promise<void>[] = [];
for (const event of this._fallback.pendingEvents) {
addEventPromises.push(this._compression.addEvent(event));
}

// We switch over to the compression buffer immediately - any further events will be added
// after the previously buffered ones
this._used = this._compression;

// Wait for original events to be re-added before resolving
await Promise.all(addEventPromises);
}
}

class EventBufferArray implements EventBuffer {
private _events: RecordingEvent[];

Expand Down Expand Up @@ -119,6 +185,34 @@ export class EventBufferCompressionWorker implements EventBuffer {
return this._pendingEvents;
}

/**
* Ensure the worker is ready (or not).
* This will either resolve when the worker is ready, or reject if an error occured.
*/
public ensureReady(): Promise<void> {
return new Promise((resolve, reject) => {
this._worker.addEventListener(
'message',
({ data }: MessageEvent) => {
if (data.success) {
resolve();
} else {
reject();
}
},
{ once: true },
);

this._worker.addEventListener(
'error',
error => {
reject(error);
},
{ once: true },
);
});
}

/**
* Destroy the event buffer.
*/
Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/types.ts
Expand Up @@ -225,7 +225,7 @@ export interface EventBuffer {
/**
* Add an event to the event buffer.
*
* Returns true if event was successfully added.
* Returns a promise that resolves if the event was successfully added, else rejects.
*/
addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult>;

Expand Down
2 changes: 1 addition & 1 deletion packages/replay/src/worker/worker.js

Large diffs are not rendered by default.

0 comments on commit b83e7e1

Please sign in to comment.