Skip to content

Commit 751ec4a

Browse files
committed
feat(replay): Keep the last checkout when in error mode
1 parent 5021ea0 commit 751ec4a

22 files changed

+331
-353
lines changed

packages/replay/.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ module.exports = {
1313
// TODO: figure out if we need a worker-specific tsconfig
1414
project: ['tsconfig.worker.json'],
1515
},
16+
rules: {
17+
// We cannot use backticks, as that conflicts with the stringified worker
18+
'prefer-template': 'off',
19+
},
1620
},
1721
{
1822
files: ['src/worker/**/*.js'],

packages/replay/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
"@babel/core": "^7.17.5",
4949
"@types/pako": "^2.0.0",
5050
"jsdom-worker": "^0.2.1",
51-
"pako": "^2.0.4",
51+
"pako": "2.1.0",
5252
"rrweb": "1.1.3",
5353
"tslib": "^1.9.3"
5454
},
Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,75 @@
1-
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
1+
import type { ReplayRecordingData } from '@sentry/types';
2+
3+
import type { EventBuffer, RecordingEvent } from '../types';
4+
5+
interface EventsGroup {
6+
checkoutTimestamp: number;
7+
events: RecordingEvent[];
8+
}
29

310
/**
411
* A basic event buffer that does not do any compression.
512
* Used as fallback if the compression worker cannot be loaded or is disabled.
613
*/
714
export class EventBufferArray implements EventBuffer {
8-
private _events: RecordingEvent[];
15+
private _events: EventsGroup[];
916

1017
public constructor() {
1118
this._events = [];
1219
}
1320

1421
/** @inheritdoc */
1522
public get pendingLength(): number {
16-
return this._events.length;
23+
return this.pendingEvents.length;
1724
}
1825

19-
/**
20-
* Returns the raw events that are buffered. In `EventBufferArray`, this is the
21-
* same as `this._events`.
22-
*/
26+
/** @inheritdoc */
2327
public get pendingEvents(): RecordingEvent[] {
24-
return this._events;
28+
return this._events.reduce((acc, { events }) => [...events, ...acc], [] as RecordingEvent[]);
2529
}
2630

2731
/** @inheritdoc */
28-
public destroy(): void {
29-
this._clear();
32+
public getFirstCheckoutTimestamp(): number | null {
33+
return (this._events[0] && this._events[0].checkoutTimestamp) || null;
3034
}
3135

3236
/** @inheritdoc */
33-
public async addEvent(event: RecordingEvent): Promise<AddEventResult> {
34-
this._events.push(event);
35-
return;
37+
public destroy(): void {
38+
this.clear();
3639
}
3740

3841
/** @inheritdoc */
39-
public async clear(untilPos?: number): Promise<void> {
40-
this._clear(untilPos);
42+
public addEvent(event: RecordingEvent, isCheckout?: boolean): void {
43+
if (isCheckout || this._events.length === 0) {
44+
const group: EventsGroup = {
45+
checkoutTimestamp: event.timestamp,
46+
events: [event],
47+
};
48+
this._events.unshift(group);
49+
} else {
50+
this._events[0].events.push(event);
51+
}
4152
}
4253

4354
/** @inheritdoc */
44-
public finish(): Promise<string> {
45-
return new Promise<string>(resolve => {
46-
// Make a copy of the events array reference and immediately clear the
47-
// events member so that we do not lose new events while uploading
48-
// attachment.
49-
const eventsRet = this._events;
50-
resolve(JSON.stringify(eventsRet));
51-
this._clear();
52-
});
53-
}
54-
55-
/** Clear all events. */
56-
private _clear(untilPos?: number): void {
57-
if (untilPos) {
58-
this._events.splice(0, untilPos);
55+
public clear(keepLastCheckout?: boolean): void {
56+
if (keepLastCheckout) {
57+
this._events.splice(1);
5958
} else {
6059
this._events = [];
6160
}
6261
}
62+
63+
/** @inheritdoc */
64+
public finish(): Promise<ReplayRecordingData> {
65+
const pendingEvents = this.pendingEvents.slice();
66+
this.clear();
67+
68+
return Promise.resolve(this._finishRecording(pendingEvents));
69+
}
70+
71+
/** Finish in a sync manner. */
72+
protected _finishRecording(events: RecordingEvent[]): ReplayRecordingData {
73+
return JSON.stringify(events);
74+
}
6375
}

packages/replay/src/eventBuffer/EventBufferCompressionWorker.ts

Lines changed: 19 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,22 @@
1+
import type { ReplayRecordingData } from '@sentry/types';
12
import { logger } from '@sentry/utils';
23

3-
import type { AddEventResult, EventBuffer, RecordingEvent, WorkerRequest, WorkerResponse } from '../types';
4+
import type { RecordingEvent, WorkerRequest, WorkerResponse } from '../types';
5+
import { EventBufferArray } from './EventBufferArray';
46

57
/**
68
* Event buffer that uses a web worker to compress events.
79
* Exported only for testing.
810
*/
9-
export class EventBufferCompressionWorker implements EventBuffer {
10-
/**
11-
* Keeps track of the list of events since the last flush that have not been compressed.
12-
* For example, page is reloaded and a flush attempt is made, but
13-
* `finish()` (and thus the flush), does not complete.
14-
*/
15-
public _pendingEvents: RecordingEvent[];
16-
11+
export class EventBufferCompressionWorker extends EventBufferArray {
1712
private _worker: Worker;
18-
private _eventBufferItemLength: number = 0;
13+
1914
private _id: number = 0;
2015

2116
public constructor(worker: Worker) {
17+
super();
2218
this._worker = worker;
23-
this._pendingEvents = [];
24-
}
25-
26-
/**
27-
* The number of raw events that are buffered. This may not be the same as
28-
* the number of events that have been compresed in the worker because
29-
* `addEvent` is async.
30-
*/
31-
public get pendingLength(): number {
32-
return this._eventBufferItemLength;
33-
}
34-
35-
/**
36-
* Returns a list of the raw recording events that are being compressed.
37-
*/
38-
public get pendingEvents(): RecordingEvent[] {
39-
return this._pendingEvents;
4019
}
41-
4220
/**
4321
* Ensure the worker is ready (or not).
4422
* This will either resolve when the worker is ready, or reject if an error occured.
@@ -67,55 +45,27 @@ export class EventBufferCompressionWorker implements EventBuffer {
6745
});
6846
}
6947

70-
/**
71-
* Destroy the event buffer.
72-
*/
48+
/** @inheritdoc */
7349
public destroy(): void {
7450
__DEBUG_BUILD__ && logger.log('[Replay] Destroying compression worker');
7551
this._worker.terminate();
76-
}
77-
78-
/**
79-
* Add an event to the event buffer.
80-
*
81-
* Returns true if event was successfuly received and processed by worker.
82-
*/
83-
public addEvent(event: RecordingEvent): Promise<AddEventResult> {
84-
this.pendingEvents.push(event);
85-
return this._sendEventToWorker(event);
86-
}
87-
88-
/** @inheritdoc */
89-
public clear(untilPos?: number): Promise<void> {
90-
this._clear(untilPos);
91-
92-
// TODO FN: Clear up to pos
93-
94-
// This will clear the queue of events that are waiting to be compressed
95-
return this._postMessage({
96-
id: this._getAndIncrementId(),
97-
method: 'init',
98-
args: [],
99-
});
52+
super.destroy();
10053
}
10154

10255
/**
10356
* Finish the event buffer and return the compressed data.
10457
*/
105-
public finish(): Promise<Uint8Array> {
106-
this._clear();
58+
public async finish(): Promise<ReplayRecordingData> {
59+
const pendingEvents = this.pendingEvents.slice();
10760

108-
return this._finishRequest(this._getAndIncrementId());
109-
}
61+
this.clear();
11062

111-
/**
112-
* Clear all pending events up to the given event pos.
113-
*/
114-
private _clear(untilPos?: number): void {
115-
if (untilPos) {
116-
this._pendingEvents.splice(0, untilPos);
117-
} else {
118-
this._pendingEvents = [];
63+
try {
64+
return await this._compressEvents(this._getAndIncrementId(), pendingEvents);
65+
} catch (error) {
66+
__DEBUG_BUILD__ && logger.error('[Replay] Error when trying to compress events', error);
67+
// fall back to uncompressed
68+
return this._finishRecording(pendingEvents);
11969
}
12070
}
12171

@@ -165,36 +115,11 @@ export class EventBufferCompressionWorker implements EventBuffer {
165115
});
166116
}
167117

168-
/**
169-
* Send the event to the worker.
170-
*/
171-
private async _sendEventToWorker(event: RecordingEvent): Promise<AddEventResult> {
172-
const promise = this._postMessage<void>({
173-
id: this._getAndIncrementId(),
174-
method: 'addEvent',
175-
args: [event],
176-
});
177-
178-
// XXX: See note in `get length()`
179-
this._eventBufferItemLength++;
180-
181-
return promise;
182-
}
183-
184118
/**
185119
* Finish the request and return the compressed data from the worker.
186120
*/
187-
private async _finishRequest(id: number): Promise<Uint8Array> {
188-
const promise = this._postMessage<Uint8Array>({ id, method: 'finish', args: [] });
189-
190-
// XXX: See note in `get length()`
191-
this._eventBufferItemLength = 0;
192-
193-
await promise;
194-
195-
this._pendingEvents = [];
196-
197-
return promise;
121+
private async _compressEvents(id: number, events: RecordingEvent[]): Promise<Uint8Array> {
122+
return this._postMessage<Uint8Array>({ id, method: 'compress', args: [events] });
198123
}
199124

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

packages/replay/src/eventBuffer/EventBufferProxy.ts

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { ReplayRecordingData } from '@sentry/types';
22
import { logger } from '@sentry/utils';
33

4-
import type { AddEventResult, EventBuffer, RecordingEvent } from '../types';
4+
import type { EventBuffer, RecordingEvent } from '../types';
55
import { EventBufferArray } from './EventBufferArray';
66
import { EventBufferCompressionWorker } from './EventBufferCompressionWorker';
77

@@ -40,20 +40,25 @@ export class EventBufferProxy implements EventBuffer {
4040
}
4141

4242
/** @inheritdoc */
43-
public addEvent(event: RecordingEvent): Promise<AddEventResult> {
44-
return this._used.addEvent(event);
43+
public addEvent(event: RecordingEvent, isCheckout?: boolean): void {
44+
return this._used.addEvent(event, isCheckout);
4545
}
4646

4747
/** @inheritdoc */
48-
public clear(untilPos?: number): Promise<void> {
49-
return this._used.clear(untilPos);
48+
public clear(keepLastCheckout?: boolean): void {
49+
return this._used.clear(keepLastCheckout);
5050
}
5151

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

57+
/** @inheritdoc */
58+
public getFirstCheckoutTimestamp(): number | null {
59+
return this._used.getFirstCheckoutTimestamp();
60+
}
61+
5762
/** Ensure the worker has loaded. */
5863
private async _ensureWorkerIsLoaded(): Promise<void> {
5964
try {
@@ -67,16 +72,14 @@ export class EventBufferProxy implements EventBuffer {
6772

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

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

79-
// Wait for original events to be re-added before resolving
80-
await Promise.all(addEventPromises);
83+
this._fallback.clear();
8184
}
8285
}

packages/replay/src/replay.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { createEventBuffer } from './eventBuffer';
1111
import { getSession } from './session/getSession';
1212
import { saveSession } from './session/saveSession';
1313
import type {
14-
AddEventResult,
1514
AddUpdateCallback,
1615
AllPerformanceEntry,
1716
EventBuffer,
@@ -678,12 +677,12 @@ export class ReplayContainer implements ReplayContainerInterface {
678677
* Observed performance events are added to `this.performanceEvents`. These
679678
* are included in the replay event before it is finished and sent to Sentry.
680679
*/
681-
private _addPerformanceEntries(): Promise<Array<AddEventResult | null>> {
680+
private _addPerformanceEntries(): void {
682681
// Copy and reset entries before processing
683682
const entries = [...this.performanceEvents];
684683
this.performanceEvents = [];
685684

686-
return Promise.all(createPerformanceSpans(this, createPerformanceEntries(entries)));
685+
createPerformanceSpans(this, createPerformanceEntries(entries));
687686
}
688687

689688
/**

0 commit comments

Comments
 (0)