Skip to content

Commit 770a33d

Browse files
authored
feat(replay): Change addEvent to be async (#6695)
`addEvent` should be async as it waits for a response from Compression worker. We don't necessarily always await `addEvent()`, but it's more correct that it is marked as `async` and can maybe save some headaches down the line.
1 parent 2aa4e94 commit 770a33d

File tree

12 files changed

+105
-86
lines changed

12 files changed

+105
-86
lines changed

packages/replay/src/eventBuffer.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22
// TODO: figure out member access types and remove the line above
33

44
import { captureException } from '@sentry/core';
5-
import type { ReplayRecordingData } from '@sentry/types';
65
import { logger } from '@sentry/utils';
76

8-
import type { EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from './types';
7+
import type { AddEventResult, EventBuffer, RecordingEvent, WorkerRequest } from './types';
98
import workerString from './worker/worker.js';
109

1110
interface CreateEventBufferParams {
@@ -54,13 +53,14 @@ class EventBufferArray implements EventBuffer {
5453
this._events = [];
5554
}
5655

57-
public addEvent(event: RecordingEvent, isCheckout?: boolean): void {
56+
public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
5857
if (isCheckout) {
5958
this._events = [event];
6059
return;
6160
}
6261

6362
this._events.push(event);
63+
return;
6464
}
6565

6666
public finish(): Promise<string> {
@@ -107,8 +107,10 @@ export class EventBufferCompressionWorker implements EventBuffer {
107107

108108
/**
109109
* Add an event to the event buffer.
110+
*
111+
* Returns true if event was successfuly received and processed by worker.
110112
*/
111-
public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<ReplayRecordingData> {
113+
public async addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult> {
112114
if (isCheckout) {
113115
// This event is a checkout, make sure worker buffer is cleared before
114116
// proceeding.
@@ -132,7 +134,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
132134
/**
133135
* Post message to worker and wait for response before resolving promise.
134136
*/
135-
private _postMessage({ id, method, args }: WorkerRequest): Promise<WorkerResponse['response']> {
137+
private _postMessage<T>({ id, method, args }: WorkerRequest): Promise<T> {
136138
return new Promise((resolve, reject) => {
137139
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
138140
const listener = ({ data }: MessageEvent) => {
@@ -178,8 +180,8 @@ export class EventBufferCompressionWorker implements EventBuffer {
178180
/**
179181
* Send the event to the worker.
180182
*/
181-
private _sendEventToWorker(event: RecordingEvent): Promise<ReplayRecordingData> {
182-
const promise = this._postMessage({
183+
private async _sendEventToWorker(event: RecordingEvent): Promise<AddEventResult> {
184+
const promise = this._postMessage<void>({
183185
id: this._getAndIncrementId(),
184186
method: 'addEvent',
185187
args: [event],
@@ -195,12 +197,12 @@ export class EventBufferCompressionWorker implements EventBuffer {
195197
* Finish the request and return the compressed data from the worker.
196198
*/
197199
private async _finishRequest(id: number): Promise<Uint8Array> {
198-
const promise = this._postMessage({ id, method: 'finish', args: [] });
200+
const promise = this._postMessage<Uint8Array>({ id, method: 'finish', args: [] });
199201

200202
// XXX: See note in `get length()`
201203
this._eventBufferItemLength = 0;
202204

203-
return promise as Promise<Uint8Array>;
205+
return promise;
204206
}
205207

206208
/** Get the current ID and increment it for the next call. */

packages/replay/src/replay.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { createEventBuffer } from './eventBuffer';
2222
import { getSession } from './session/getSession';
2323
import { saveSession } from './session/saveSession';
2424
import type {
25+
AddEventResult,
2526
AddUpdateCallback,
2627
AllPerformanceEntry,
2728
EventBuffer,
@@ -450,7 +451,7 @@ export class ReplayContainer implements ReplayContainerInterface {
450451

451452
// We need to clear existing events on a checkout, otherwise they are
452453
// incremental event updates and should be appended
453-
addEvent(this, event, isCheckout);
454+
void addEvent(this, event, isCheckout);
454455

455456
// Different behavior for full snapshots (type=2), ignore other event types
456457
// See https://github.com/rrweb-io/rrweb/blob/d8f9290ca496712aa1e7d472549480c4e7876594/packages/rrweb/src/types.ts#L16
@@ -556,7 +557,7 @@ export class ReplayContainer implements ReplayContainerInterface {
556557
}
557558

558559
this.addUpdate(() => {
559-
addEvent(this, {
560+
void addEvent(this, {
560561
type: EventType.Custom,
561562
// TODO: We were converting from ms to seconds for breadcrumbs, spans,
562563
// but maybe we should just keep them as milliseconds
@@ -674,7 +675,7 @@ export class ReplayContainer implements ReplayContainerInterface {
674675
*/
675676
createCustomBreadcrumb(breadcrumb: Breadcrumb): void {
676677
this.addUpdate(() => {
677-
addEvent(this, {
678+
void addEvent(this, {
678679
type: EventType.Custom,
679680
timestamp: breadcrumb.timestamp || 0,
680681
data: {
@@ -689,12 +690,12 @@ export class ReplayContainer implements ReplayContainerInterface {
689690
* Observed performance events are added to `this.performanceEvents`. These
690691
* are included in the replay event before it is finished and sent to Sentry.
691692
*/
692-
addPerformanceEntries(): void {
693+
addPerformanceEntries(): Promise<Array<AddEventResult | null>> {
693694
// Copy and reset entries before processing
694695
const entries = [...this.performanceEvents];
695696
this.performanceEvents = [];
696697

697-
createPerformanceSpans(this, createPerformanceEntries(entries));
698+
return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries)));
698699
}
699700

700701
/**

packages/replay/src/types.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,11 @@ export interface WorkerResponse {
4848
id: number;
4949
method: string;
5050
success: boolean;
51-
response: ReplayRecordingData;
51+
response: unknown;
5252
}
5353

54+
export type AddEventResult = void;
55+
5456
export interface SampleRates {
5557
/**
5658
* The sample rate for session-long replays. 1.0 will record all sessions and
@@ -210,8 +212,22 @@ export interface Session {
210212

211213
export interface EventBuffer {
212214
readonly length: number;
215+
216+
/**
217+
* Destroy the event buffer.
218+
*/
213219
destroy(): void;
214-
addEvent(event: RecordingEvent, isCheckout?: boolean): void;
220+
221+
/**
222+
* Add an event to the event buffer.
223+
*
224+
* Returns true if event was successfully added.
225+
*/
226+
addEvent(event: RecordingEvent, isCheckout?: boolean): Promise<AddEventResult>;
227+
228+
/**
229+
* Clears and returns the contents and the buffer.
230+
*/
215231
finish(): Promise<ReplayRecordingData>;
216232
}
217233

packages/replay/src/util/addEvent.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
11
import { SESSION_IDLE_DURATION } from '../constants';
2-
import type { RecordingEvent, ReplayContainer } from '../types';
2+
import type { AddEventResult, RecordingEvent, ReplayContainer } from '../types';
33

44
/**
55
* Add an event to the event buffer
66
*/
7-
export function addEvent(replay: ReplayContainer, event: RecordingEvent, isCheckout?: boolean): void {
7+
export async function addEvent(
8+
replay: ReplayContainer,
9+
event: RecordingEvent,
10+
isCheckout?: boolean,
11+
): Promise<AddEventResult | null> {
812
if (!replay.eventBuffer) {
913
// This implies that `_isEnabled` is false
10-
return;
14+
return null;
1115
}
1216

1317
if (replay.isPaused()) {
1418
// Do not add to event buffer when recording is paused
15-
return;
19+
return null;
1620
}
1721

1822
// TODO: sadness -- we will want to normalize timestamps to be in ms -
@@ -25,7 +29,7 @@ export function addEvent(replay: ReplayContainer, event: RecordingEvent, isCheck
2529
// comes back to trigger a new session. The performance entries rely on
2630
// `performance.timeOrigin`, which is when the page first opened.
2731
if (timestampInMs + SESSION_IDLE_DURATION < new Date().getTime()) {
28-
return;
32+
return null;
2933
}
3034

3135
// Only record earliest event if a new session was created, otherwise it
@@ -35,5 +39,5 @@ export function addEvent(replay: ReplayContainer, event: RecordingEvent, isCheck
3539
replay.getContext().earliestEvent = timestampInMs;
3640
}
3741

38-
replay.eventBuffer.addEvent(event, isCheckout);
42+
return replay.eventBuffer.addEvent(event, isCheckout);
3943
}

packages/replay/src/util/addMemoryEntry.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { WINDOW } from '../constants';
2-
import type { ReplayContainer, ReplayPerformanceEntry } from '../types';
2+
import type { AddEventResult, ReplayContainer, ReplayPerformanceEntry } from '../types';
33
import { createPerformanceSpans } from './createPerformanceSpans';
44

55
type ReplayMemoryEntry = ReplayPerformanceEntry & { data: { memory: MemoryInfo } };
@@ -14,15 +14,18 @@ interface MemoryInfo {
1414
* Create a "span" for the total amount of memory being used by JS objects
1515
* (including v8 internal objects).
1616
*/
17-
export function addMemoryEntry(replay: ReplayContainer): void {
17+
export async function addMemoryEntry(replay: ReplayContainer): Promise<Array<AddEventResult | null>> {
1818
// window.performance.memory is a non-standard API and doesn't work on all browsers, so we try-catch this
1919
try {
20-
createPerformanceSpans(replay, [
21-
// @ts-ignore memory doesn't exist on type Performance as the API is non-standard (we check that it exists above)
22-
createMemoryEntry(WINDOW.performance.memory),
23-
]);
20+
return Promise.all(
21+
createPerformanceSpans(replay, [
22+
// @ts-ignore memory doesn't exist on type Performance as the API is non-standard (we check that it exists above)
23+
createMemoryEntry(WINDOW.performance.memory),
24+
]),
25+
);
2426
} catch (error) {
2527
// Do nothing
28+
return [];
2629
}
2730
}
2831

packages/replay/src/util/createPerformanceSpans.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import { EventType } from 'rrweb';
22

3-
import type { ReplayContainer, ReplayPerformanceEntry } from '../types';
3+
import type { AddEventResult, ReplayContainer, ReplayPerformanceEntry } from '../types';
44
import { addEvent } from './addEvent';
55

66
/**
77
* Create a "span" for each performance entry. The parent transaction is `this.replayEvent`.
88
*/
9-
export function createPerformanceSpans(replay: ReplayContainer, entries: ReplayPerformanceEntry[]): void {
10-
entries.map(({ type, start, end, name, data }) =>
9+
export function createPerformanceSpans(
10+
replay: ReplayContainer,
11+
entries: ReplayPerformanceEntry[],
12+
): Promise<AddEventResult | null>[] {
13+
return entries.map(({ type, start, end, name, data }) =>
1114
addEvent(replay, {
1215
type: EventType.Custom,
1316
timestamp: start,

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.

packages/replay/test/integration/flush.test.ts

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -182,37 +182,39 @@ describe('Integration | flush', () => {
182182
});
183183

184184
// Add this to test that segment ID increases
185-
mockAddPerformanceEntries.mockImplementationOnce(() => {
186-
createPerformanceSpans(
187-
replay,
188-
createPerformanceEntries([
189-
{
190-
name: 'https://sentry.io/foo.js',
191-
entryType: 'resource',
192-
startTime: 176.59999990463257,
193-
duration: 5.600000023841858,
194-
initiatorType: 'link',
195-
nextHopProtocol: 'h2',
196-
workerStart: 177.5,
197-
redirectStart: 0,
198-
redirectEnd: 0,
199-
fetchStart: 177.69999992847443,
200-
domainLookupStart: 177.69999992847443,
201-
domainLookupEnd: 177.69999992847443,
202-
connectStart: 177.69999992847443,
203-
connectEnd: 177.69999992847443,
204-
secureConnectionStart: 177.69999992847443,
205-
requestStart: 177.5,
206-
responseStart: 181,
207-
responseEnd: 182.19999992847443,
208-
transferSize: 0,
209-
encodedBodySize: 0,
210-
decodedBodySize: 0,
211-
serverTiming: [],
212-
} as unknown as PerformanceResourceTiming,
213-
]),
214-
);
215-
});
185+
mockAddPerformanceEntries.mockImplementationOnce(() =>
186+
Promise.all(
187+
createPerformanceSpans(
188+
replay,
189+
createPerformanceEntries([
190+
{
191+
name: 'https://sentry.io/foo.js',
192+
entryType: 'resource',
193+
startTime: 176.59999990463257,
194+
duration: 5.600000023841858,
195+
initiatorType: 'link',
196+
nextHopProtocol: 'h2',
197+
workerStart: 177.5,
198+
redirectStart: 0,
199+
redirectEnd: 0,
200+
fetchStart: 177.69999992847443,
201+
domainLookupStart: 177.69999992847443,
202+
domainLookupEnd: 177.69999992847443,
203+
connectStart: 177.69999992847443,
204+
connectEnd: 177.69999992847443,
205+
secureConnectionStart: 177.69999992847443,
206+
requestStart: 177.5,
207+
responseStart: 181,
208+
responseEnd: 182.19999992847443,
209+
transferSize: 0,
210+
encodedBodySize: 0,
211+
decodedBodySize: 0,
212+
serverTiming: [],
213+
} as unknown as PerformanceResourceTiming,
214+
]),
215+
),
216+
),
217+
);
216218
// flush #5 @ t=25s - debounced flush calls `flush`
217219
// 20s + `flushMinDelay` which is 5 seconds
218220
await advanceTimers(DEFAULT_FLUSH_MIN_DELAY);

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ describe('Unit | eventBuffer', () => {
5353
}) as EventBufferCompressionWorker;
5454

5555
buffer.addEvent(TEST_EVENT);
56+
// @ts-ignore make sure it handles invalid data
57+
buffer.addEvent(undefined);
5658
buffer.addEvent(TEST_EVENT);
5759

5860
const result = await buffer.finish();

packages/replay/test/unit/worker/Compressor.test.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -26,28 +26,16 @@ describe('Unit | worker | Compressor', () => {
2626
expect(restored).toBe(JSON.stringify(events));
2727
});
2828

29-
it('ignores undefined events', () => {
29+
it('throws on invalid/undefined events', () => {
3030
const compressor = new Compressor();
3131

32-
const events = [
33-
{
34-
id: 1,
35-
foo: ['bar', 'baz'],
36-
},
37-
undefined,
38-
{
39-
id: 2,
40-
foo: [false],
41-
},
42-
] as Record<string, any>[];
43-
44-
events.forEach(event => compressor.addEvent(event));
32+
// @ts-ignore ignoring type for test
33+
expect(() => void compressor.addEvent(undefined)).toThrow();
4534

4635
const compressed = compressor.finish();
4736

4837
const restored = pako.inflate(compressed, { to: 'string' });
4938

50-
const expected = [events[0], events[2]];
51-
expect(restored).toBe(JSON.stringify(expected));
39+
expect(restored).toBe(JSON.stringify([]));
5240
});
5341
});

packages/replay/worker/src/Compressor.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,17 @@ export class Compressor {
2727

2828
public addEvent(data: Record<string, unknown>): void {
2929
if (!data) {
30-
return;
30+
throw new Error('Adding invalid event');
3131
}
3232
// If the event is not the first event, we need to prefix it with a `,` so
3333
// that we end up with a list of events
3434
const prefix = this.added > 0 ? ',' : '';
3535
// TODO: We may want Z_SYNC_FLUSH or Z_FULL_FLUSH (not sure the difference)
3636
// Using NO_FLUSH here for now as we can create many attachments that our
3737
// web UI will get API rate limited.
38-
this.deflate.push(prefix + JSON.stringify(data), constants.Z_NO_FLUSH);
39-
this.added++;
38+
this.deflate.push(prefix + JSON.stringify(data), constants.Z_SYNC_FLUSH);
4039

41-
return;
40+
this.added++;
4241
}
4342

4443
public finish(): Uint8Array {

0 commit comments

Comments
 (0)