1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2
- // TODO: figure out member access types and remove the line above
3
-
4
1
import type { ReplayRecordingData } from '@sentry/types' ;
5
2
import { logger } from '@sentry/utils' ;
6
3
7
- import type { AddEventResult , EventBuffer , RecordingEvent , WorkerRequest } from './types' ;
8
- import workerString from './worker/worker.js' ;
9
-
10
- interface CreateEventBufferParams {
11
- useCompression : boolean ;
12
- }
13
-
14
- /**
15
- * Create an event buffer for replays.
16
- */
17
- export function createEventBuffer ( { useCompression } : CreateEventBufferParams ) : EventBuffer {
18
- // eslint-disable-next-line no-restricted-globals
19
- if ( useCompression && window . Worker ) {
20
- const workerBlob = new Blob ( [ workerString ] ) ;
21
- const workerUrl = URL . createObjectURL ( workerBlob ) ;
22
-
23
- __DEBUG_BUILD__ && logger . log ( '[Replay] Using compression worker' ) ;
24
- const worker = new Worker ( workerUrl ) ;
25
- return new EventBufferProxy ( worker ) ;
26
- }
27
-
28
- __DEBUG_BUILD__ && logger . log ( '[Replay] Using simple buffer' ) ;
29
- return new EventBufferArray ( ) ;
30
- }
31
-
32
- /**
33
- * This proxy will try to use the compression worker, and fall back to use the simple buffer if an error occurs there.
34
- * This can happen e.g. if the worker cannot be loaded.
35
- * Exported only for testing.
36
- */
37
- export class EventBufferProxy implements EventBuffer {
38
- private _fallback : EventBufferArray ;
39
- private _compression : EventBufferCompressionWorker ;
40
- private _used : EventBuffer ;
41
-
42
- public constructor ( worker : Worker ) {
43
- this . _fallback = new EventBufferArray ( ) ;
44
- this . _compression = new EventBufferCompressionWorker ( worker ) ;
45
- this . _used = this . _fallback ;
46
-
47
- void this . _ensureWorkerIsLoaded ( ) ;
48
- }
49
-
50
- /** @inheritDoc */
51
- public get pendingLength ( ) : number {
52
- return this . _used . pendingLength ;
53
- }
54
-
55
- /** @inheritDoc */
56
- public get pendingEvents ( ) : RecordingEvent [ ] {
57
- return this . _used . pendingEvents ;
58
- }
59
-
60
- /** @inheritDoc */
61
- public destroy ( ) : void {
62
- this . _fallback . destroy ( ) ;
63
- this . _compression . destroy ( ) ;
64
- }
65
-
66
- /**
67
- * Add an event to the event buffer.
68
- *
69
- * Returns true if event was successfully added.
70
- */
71
- public addEvent ( event : RecordingEvent , isCheckout ?: boolean ) : Promise < AddEventResult > {
72
- return this . _used . addEvent ( event , isCheckout ) ;
73
- }
74
-
75
- /** @inheritDoc */
76
- public finish ( ) : Promise < ReplayRecordingData > {
77
- return this . _used . finish ( ) ;
78
- }
79
-
80
- /** Ensure the worker has loaded. */
81
- private async _ensureWorkerIsLoaded ( ) : Promise < void > {
82
- try {
83
- await this . _compression . ensureReady ( ) ;
84
- } catch ( error ) {
85
- // If the worker fails to load, we fall back to the simple buffer.
86
- // Nothing more to do from our side here
87
- __DEBUG_BUILD__ && logger . log ( '[Replay] Failed to load the compression worker, falling back to simple buffer' ) ;
88
- return ;
89
- }
90
-
91
- // Compression worker is ready, we can use it
92
- // Now we need to switch over the array buffer to the compression worker
93
- const addEventPromises : Promise < void > [ ] = [ ] ;
94
- for ( const event of this . _fallback . pendingEvents ) {
95
- addEventPromises . push ( this . _compression . addEvent ( event ) ) ;
96
- }
97
-
98
- // We switch over to the compression buffer immediately - any further events will be added
99
- // after the previously buffered ones
100
- this . _used = this . _compression ;
101
-
102
- // Wait for original events to be re-added before resolving
103
- await Promise . all ( addEventPromises ) ;
104
- }
105
- }
106
-
107
- class EventBufferArray implements EventBuffer {
108
- private _events : RecordingEvent [ ] ;
109
-
110
- public constructor ( ) {
111
- this . _events = [ ] ;
112
- }
113
-
114
- public get pendingLength ( ) : number {
115
- return this . _events . length ;
116
- }
117
-
118
- /**
119
- * Returns the raw events that are buffered. In `EventBufferArray`, this is the
120
- * same as `this._events`.
121
- */
122
- public get pendingEvents ( ) : RecordingEvent [ ] {
123
- return this . _events ;
124
- }
125
-
126
- public destroy ( ) : void {
127
- this . _events = [ ] ;
128
- }
129
-
130
- public async addEvent ( event : RecordingEvent , isCheckout ?: boolean ) : Promise < AddEventResult > {
131
- if ( isCheckout ) {
132
- this . _events = [ event ] ;
133
- return ;
134
- }
135
-
136
- this . _events . push ( event ) ;
137
- return ;
138
- }
139
-
140
- public finish ( ) : Promise < string > {
141
- return new Promise < string > ( resolve => {
142
- // Make a copy of the events array reference and immediately clear the
143
- // events member so that we do not lose new events while uploading
144
- // attachment.
145
- const eventsRet = this . _events ;
146
- this . _events = [ ] ;
147
- resolve ( JSON . stringify ( eventsRet ) ) ;
148
- } ) ;
149
- }
150
- }
4
+ import type { AddEventResult , EventBuffer , RecordingEvent , WorkerRequest , WorkerResponse } from '../types' ;
151
5
152
6
/**
153
7
* Event buffer that uses a web worker to compress events.
@@ -164,6 +18,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
164
18
private _worker : Worker ;
165
19
private _eventBufferItemLength : number = 0 ;
166
20
private _id : number = 0 ;
21
+ private _ensureReadyPromise ?: Promise < void > ;
167
22
168
23
public constructor ( worker : Worker ) {
169
24
this . _worker = worker ;
@@ -190,11 +45,16 @@ export class EventBufferCompressionWorker implements EventBuffer {
190
45
* This will either resolve when the worker is ready, or reject if an error occured.
191
46
*/
192
47
public ensureReady ( ) : Promise < void > {
193
- return new Promise ( ( resolve , reject ) => {
48
+ // Ensure we only check once
49
+ if ( this . _ensureReadyPromise ) {
50
+ return this . _ensureReadyPromise ;
51
+ }
52
+
53
+ this . _ensureReadyPromise = new Promise ( ( resolve , reject ) => {
194
54
this . _worker . addEventListener (
195
55
'message' ,
196
56
( { data } : MessageEvent ) => {
197
- if ( data . success ) {
57
+ if ( ( data as WorkerResponse ) . success ) {
198
58
resolve ( ) ;
199
59
} else {
200
60
reject ( ) ;
@@ -211,6 +71,8 @@ export class EventBufferCompressionWorker implements EventBuffer {
211
71
{ once : true } ,
212
72
) ;
213
73
} ) ;
74
+
75
+ return this . _ensureReadyPromise ;
214
76
}
215
77
216
78
/**
@@ -248,39 +110,46 @@ export class EventBufferCompressionWorker implements EventBuffer {
248
110
/**
249
111
* Finish the event buffer and return the compressed data.
250
112
*/
251
- public finish ( ) : Promise < Uint8Array > {
252
- return this . _finishRequest ( this . _getAndIncrementId ( ) ) ;
113
+ public async finish ( ) : Promise < ReplayRecordingData > {
114
+ try {
115
+ return await this . _finishRequest ( this . _getAndIncrementId ( ) ) ;
116
+ } catch ( error ) {
117
+ __DEBUG_BUILD__ && logger . error ( '[Replay] Error when trying to compress events' , error ) ;
118
+ // fall back to uncompressed
119
+ const events = this . pendingEvents ;
120
+ return JSON . stringify ( events ) ;
121
+ }
253
122
}
254
123
255
124
/**
256
125
* Post message to worker and wait for response before resolving promise.
257
126
*/
258
127
private _postMessage < T > ( { id, method, args } : WorkerRequest ) : Promise < T > {
259
128
return new Promise ( ( resolve , reject ) => {
260
- // eslint-disable-next-line @typescript-eslint/explicit-function-return-type
261
- const listener = ( { data } : MessageEvent ) => {
262
- if ( data . method !== method ) {
129
+ const listener = ( { data } : MessageEvent ) : void => {
130
+ const response = data as WorkerResponse ;
131
+ if ( response . method !== method ) {
263
132
return ;
264
133
}
265
134
266
135
// There can be multiple listeners for a single method, the id ensures
267
136
// that the response matches the caller.
268
- if ( data . id !== id ) {
137
+ if ( response . id !== id ) {
269
138
return ;
270
139
}
271
140
272
141
// At this point, we'll always want to remove listener regardless of result status
273
142
this . _worker . removeEventListener ( 'message' , listener ) ;
274
143
275
- if ( ! data . success ) {
144
+ if ( ! response . success ) {
276
145
// TODO: Do some error handling, not sure what
277
- __DEBUG_BUILD__ && logger . error ( '[Replay]' , data . response ) ;
146
+ __DEBUG_BUILD__ && logger . error ( '[Replay]' , response . response ) ;
278
147
279
148
reject ( new Error ( 'Error in compression worker' ) ) ;
280
149
return ;
281
150
}
282
151
283
- resolve ( data . response ) ;
152
+ resolve ( response . response as T ) ;
284
153
} ;
285
154
286
155
let stringifiedArgs ;
0 commit comments