1- /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2- // TODO: figure out member access types and remove the line above
3-
41import type { ReplayRecordingData } from '@sentry/types' ;
52import { logger } from '@sentry/utils' ;
63
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' ;
1515
1526/**
1537 * Event buffer that uses a web worker to compress events.
@@ -164,6 +18,7 @@ export class EventBufferCompressionWorker implements EventBuffer {
16418 private _worker : Worker ;
16519 private _eventBufferItemLength : number = 0 ;
16620 private _id : number = 0 ;
21+ private _ensureReadyPromise ?: Promise < void > ;
16722
16823 public constructor ( worker : Worker ) {
16924 this . _worker = worker ;
@@ -190,11 +45,16 @@ export class EventBufferCompressionWorker implements EventBuffer {
19045 * This will either resolve when the worker is ready, or reject if an error occured.
19146 */
19247 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 ) => {
19454 this . _worker . addEventListener (
19555 'message' ,
19656 ( { data } : MessageEvent ) => {
197- if ( data . success ) {
57+ if ( ( data as WorkerResponse ) . success ) {
19858 resolve ( ) ;
19959 } else {
20060 reject ( ) ;
@@ -211,6 +71,8 @@ export class EventBufferCompressionWorker implements EventBuffer {
21171 { once : true } ,
21272 ) ;
21373 } ) ;
74+
75+ return this . _ensureReadyPromise ;
21476 }
21577
21678 /**
@@ -248,39 +110,46 @@ export class EventBufferCompressionWorker implements EventBuffer {
248110 /**
249111 * Finish the event buffer and return the compressed data.
250112 */
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+ }
253122 }
254123
255124 /**
256125 * Post message to worker and wait for response before resolving promise.
257126 */
258127 private _postMessage < T > ( { id, method, args } : WorkerRequest ) : Promise < T > {
259128 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 ) {
263132 return ;
264133 }
265134
266135 // There can be multiple listeners for a single method, the id ensures
267136 // that the response matches the caller.
268- if ( data . id !== id ) {
137+ if ( response . id !== id ) {
269138 return ;
270139 }
271140
272141 // At this point, we'll always want to remove listener regardless of result status
273142 this . _worker . removeEventListener ( 'message' , listener ) ;
274143
275- if ( ! data . success ) {
144+ if ( ! response . success ) {
276145 // TODO: Do some error handling, not sure what
277- __DEBUG_BUILD__ && logger . error ( '[Replay]' , data . response ) ;
146+ __DEBUG_BUILD__ && logger . error ( '[Replay]' , response . response ) ;
278147
279148 reject ( new Error ( 'Error in compression worker' ) ) ;
280149 return ;
281150 }
282151
283- resolve ( data . response ) ;
152+ resolve ( response . response as T ) ;
284153 } ;
285154
286155 let stringifiedArgs ;
0 commit comments