Skip to content

feat(replay): Handle worker loading errors #6827

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jan 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/replay/jest.setup.ts
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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.

Loading