Skip to content

Commit 06480ea

Browse files
fix(v10/replay): Suppress Worker destroyed error on session expiry (#23654)
Backport of: #23409 Co-authored-by: sentry[bot] <39604003+sentry[bot]@users.noreply.github.com>
1 parent 630e5f5 commit 06480ea

5 files changed

Lines changed: 80 additions & 3 deletions

File tree

‎packages/replay-internal/src/eventBuffer/EventBufferProxy.ts‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type { AddEventResult, EventBuffer, EventBufferType, RecordingEvent } fro
44
import { debug } from '../util/logger';
55
import { EventBufferArray } from './EventBufferArray';
66
import { EventBufferCompressionWorker } from './EventBufferCompressionWorker';
7+
import { WorkerDestroyedError } from './error';
78

89
/**
910
* This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
@@ -130,6 +131,11 @@ export class EventBufferProxy implements EventBuffer {
130131
// Can now clear fallback buffer as it's no longer necessary
131132
this._fallback.clear();
132133
} catch (error) {
134+
// Destroying the worker (e.g. when the session expires) rejects the
135+
// in-flight requests. This is expected teardown, not a failure.
136+
if (error instanceof WorkerDestroyedError) {
137+
return;
138+
}
133139
DEBUG_BUILD && debug.exception(error, 'Failed to add events when switching buffers.');
134140
}
135141
}

‎packages/replay-internal/src/eventBuffer/WorkerHandler.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DEBUG_BUILD } from '../debug-build';
22
import type { WorkerRequest, WorkerResponse } from '../types';
33
import { debug } from '../util/logger';
4+
import { WorkerDestroyedError } from './error';
45

56
interface PendingRequest {
67
method: WorkerRequest['method'];
@@ -75,7 +76,7 @@ export class WorkerHandler {
7576
public destroy(): void {
7677
DEBUG_BUILD && debug.log('Destroying compression worker');
7778
this._worker.removeEventListener('message', this._onMessage);
78-
this._pending.forEach(pending => pending.reject(new Error('Worker destroyed')));
79+
this._pending.forEach(pending => pending.reject(new WorkerDestroyedError()));
7980
this._pending.clear();
8081
this._worker.terminate();
8182
}

‎packages/replay-internal/src/eventBuffer/error.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ export class EventBufferSizeExceededError extends Error {
66
super(`Event buffer exceeded maximum size of ${REPLAY_MAX_EVENT_BUFFER_SIZE}.`);
77
}
88
}
9+
10+
/** This error indicates that the compression worker was intentionally destroyed (e.g. on session expiry). */
11+
export class WorkerDestroyedError extends Error {
12+
public constructor() {
13+
super('Worker destroyed');
14+
}
15+
}

‎packages/replay-internal/test/unit/eventBuffer/EventBufferProxy.test.ts‎

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,55 @@ import 'jsdom-worker';
66
import type { MockInstance } from 'vitest';
77
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
88
import { EventBufferProxy } from '../../../src/eventBuffer/EventBufferProxy';
9+
import { debug } from '../../../src/util/logger';
910
import { BASE_TIMESTAMP } from '../..';
1011
import { decompress } from '../../utils/compression';
1112
import { getTestEventIncremental } from '../../utils/getTestEvent';
1213
import { createEventBuffer } from './../../../src/eventBuffer';
1314

1415
const TEST_EVENT = getTestEventIncremental({ timestamp: BASE_TIMESTAMP });
1516

17+
/**
18+
* Worker stub that only answers when the test tells it to, so the buffer can be
19+
* destroyed while the switch to the compression worker is still in flight.
20+
*/
21+
class ControlledWorker extends EventTarget {
22+
public posted: Array<{ id: number; method: string }> = [];
23+
24+
public postMessage(data: unknown): void {
25+
this.posted.push(data as { id: number; method: string });
26+
}
27+
28+
public terminate(): void {
29+
// noop
30+
}
31+
32+
/** Emit the message the worker sends once its script has loaded. */
33+
public sendReady(): void {
34+
this.dispatchEvent(new MessageEvent('message', { data: { success: true } }));
35+
}
36+
37+
/** Answer all posted requests with an unsuccessful response. */
38+
public failAll(): void {
39+
this.posted.forEach(({ id, method }) => {
40+
this.dispatchEvent(new MessageEvent('message', { data: { id, method, success: false } }));
41+
});
42+
}
43+
}
44+
1645
describe('Unit | eventBuffer | EventBufferProxy', () => {
1746
let consoleErrorSpy: MockInstance<any>;
47+
let exceptionSpy: MockInstance<any>;
1848

1949
beforeEach(() => {
2050
// Avoid logging errors to console
2151
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
52+
exceptionSpy = vi.spyOn(debug, 'exception').mockImplementation(() => {});
2253
});
2354

2455
afterEach(() => {
2556
consoleErrorSpy.mockRestore();
57+
exceptionSpy.mockRestore();
2658
});
2759

2860
it('waits for the worker to be loaded when calling finish', async function () {
@@ -67,4 +99,34 @@ describe('Unit | eventBuffer | EventBufferProxy', () => {
6799
expect(typeof result2).toBe('string');
68100
expect(result2).toEqual(JSON.stringify([TEST_EVENT, TEST_EVENT, TEST_EVENT]));
69101
});
102+
103+
it('does not report an error if the worker is destroyed while switching buffers', async function () {
104+
const worker = new ControlledWorker();
105+
const buffer = new EventBufferProxy(worker as unknown as Worker);
106+
107+
await buffer.addEvent(TEST_EVENT);
108+
109+
worker.sendReady();
110+
await vi.waitFor(() => expect(worker.posted).toHaveLength(1));
111+
112+
buffer.destroy();
113+
114+
await buffer.ensureWorkerIsLoaded();
115+
expect(exceptionSpy).not.toHaveBeenCalled();
116+
});
117+
118+
it('reports an error if adding events fails while switching buffers', async function () {
119+
const worker = new ControlledWorker();
120+
const buffer = new EventBufferProxy(worker as unknown as Worker);
121+
122+
await buffer.addEvent(TEST_EVENT);
123+
124+
worker.sendReady();
125+
await vi.waitFor(() => expect(worker.posted).toHaveLength(1));
126+
127+
worker.failAll();
128+
129+
await buffer.ensureWorkerIsLoaded();
130+
expect(exceptionSpy).toHaveBeenCalledWith(expect.any(Error), 'Failed to add events when switching buffers.');
131+
});
70132
});

‎packages/replay-internal/test/unit/eventBuffer/WorkerHandler.test.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
*/
44

55
import { describe, expect, it } from 'vitest';
6+
import { WorkerDestroyedError } from '../../../src/eventBuffer/error';
67
import { WorkerHandler } from '../../../src/eventBuffer/WorkerHandler';
78
import type { WorkerResponse } from '../../../src/types';
89

@@ -166,8 +167,8 @@ describe('Unit | eventBuffer | WorkerHandler', () => {
166167

167168
handler.destroy();
168169

169-
await expect(p1).rejects.toThrow('Worker destroyed');
170-
await expect(p2).rejects.toThrow('Worker destroyed');
170+
await expect(p1).rejects.toThrow(WorkerDestroyedError);
171+
await expect(p2).rejects.toThrow(WorkerDestroyedError);
171172
expect(worker.terminated).toBe(true);
172173
expect(worker.listenerCount).toBe(0);
173174
});

0 commit comments

Comments
 (0)