1
1
/* eslint-disable max-lines */ // TODO: We might want to split this file up
2
2
import { addGlobalEventProcessor , captureException , getCurrentHub } from '@sentry/core' ;
3
- import type { Breadcrumb , ReplayRecordingMode } from '@sentry/types' ;
3
+ import type { Breadcrumb , ReplayRecordingMode , ReplayRecordingData } from '@sentry/types' ;
4
4
import type { RateLimits } from '@sentry/utils' ;
5
5
import { addInstrumentationHandler , disabledUntil , logger } from '@sentry/utils' ;
6
6
import { EventType , record } from 'rrweb' ;
@@ -28,6 +28,7 @@ import type {
28
28
ReplayContainer as ReplayContainerInterface ,
29
29
ReplayPluginOptions ,
30
30
Session ,
31
+ FlushOptions ,
31
32
} from './types' ;
32
33
import { addEvent } from './util/addEvent' ;
33
34
import { addMemoryEntry } from './util/addMemoryEntry' ;
@@ -151,7 +152,7 @@ export class ReplayContainer implements ReplayContainerInterface {
151
152
* Creates or loads a session, attaches listeners to varying events (DOM,
152
153
* _performanceObserver, Recording, Sentry SDK, etc)
153
154
*/
154
- public start ( ) : void {
155
+ public async start ( ) : Promise < void > {
155
156
this . _setInitialState ( ) ;
156
157
157
158
this . _loadSession ( { expiry : SESSION_IDLE_DURATION } ) ;
@@ -324,7 +325,6 @@ export class ReplayContainer implements ReplayContainerInterface {
324
325
}
325
326
326
327
/**
327
- *
328
328
* Always flush via `_debouncedFlush` so that we do not have flushes triggered
329
329
* from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
330
330
* cases of mulitple flushes happening closely together.
@@ -335,7 +335,7 @@ export class ReplayContainer implements ReplayContainerInterface {
335
335
return this . _debouncedFlush . flush ( ) as Promise < void > ;
336
336
}
337
337
338
- /** Get the current sesion (=replay) ID */
338
+ /** Get the current session (=replay) ID */
339
339
public getSessionId ( ) : string | undefined {
340
340
return this . session && this . session . id ;
341
341
}
@@ -625,7 +625,7 @@ export class ReplayContainer implements ReplayContainerInterface {
625
625
// Send replay when the page/tab becomes hidden. There is no reason to send
626
626
// replay if it becomes visible, since no actions we care about were done
627
627
// while it was hidden
628
- this . _conditionalFlush ( ) ;
628
+ this . _conditionalFlush ( { finishImmediate : true } ) ;
629
629
}
630
630
631
631
/**
@@ -747,11 +747,20 @@ export class ReplayContainer implements ReplayContainerInterface {
747
747
/**
748
748
* Only flush if `this.recordingMode === 'session'`
749
749
*/
750
- private _conditionalFlush ( ) : void {
750
+ private _conditionalFlush ( options : FlushOptions = { } ) : void {
751
751
if ( this . recordingMode === 'error' ) {
752
752
return ;
753
753
}
754
754
755
+ /**
756
+ * Page is likely to unload so need to bypass debounce completely and
757
+ * synchronously retrieve pending events from buffer and send request asap.
758
+ */
759
+ if ( options . finishImmediate ) {
760
+ void this . _runFlush ( options ) ;
761
+ return ;
762
+ }
763
+
755
764
void this . flushImmediate ( ) ;
756
765
}
757
766
@@ -795,40 +804,60 @@ export class ReplayContainer implements ReplayContainerInterface {
795
804
*
796
805
* Should never be called directly, only by `flush`
797
806
*/
798
- private async _runFlush ( ) : Promise < void > {
807
+ private async _runFlush ( options : FlushOptions = { } ) : Promise < void > {
799
808
if ( ! this . session || ! this . eventBuffer ) {
800
809
__DEBUG_BUILD__ && logger . error ( '[Replay] No session or eventBuffer found to flush.' ) ;
801
810
return ;
802
811
}
803
812
804
- await this . _addPerformanceEntries ( ) ;
813
+ try {
814
+ this . _debouncedFlush . cancel ( ) ;
805
815
806
- // Check eventBuffer again, as it could have been stopped in the meanwhile
807
- if ( ! this . eventBuffer || ! this . eventBuffer . pendingLength ) {
808
- return ;
809
- }
816
+ const promises : Promise < any > [ ] = [ ] ;
810
817
811
- // Only attach memory event if eventBuffer is not empty
812
- await addMemoryEntry ( this ) ;
818
+ promises . push ( this . _addPerformanceEntries ( ) ) ;
813
819
814
- // Check eventBuffer again, as it could have been stopped in the meanwhile
815
- if ( ! this . eventBuffer ) {
816
- return ;
817
- }
820
+ // Do not continue if there are no pending events in buffer
821
+ if ( ! this . eventBuffer ?. pendingLength ) {
822
+ return ;
823
+ }
818
824
819
- try {
820
- // Note this empties the event buffer regardless of outcome of sending replay
821
- const recordingData = await this . eventBuffer . finish ( ) ;
825
+ // Only attach memory entry if eventBuffer is not empty
826
+ promises . push ( addMemoryEntry ( this ) ) ;
822
827
823
828
// NOTE: Copy values from instance members, as it's possible they could
824
829
// change before the flush finishes.
825
830
const replayId = this . session . id ;
826
831
const eventContext = this . _popEventContext ( ) ;
827
832
// Always increment segmentId regardless of outcome of sending replay
828
833
const segmentId = this . session . segmentId ++ ;
834
+
835
+ // Save session (new segment id) after we save flush data assuming either
836
+ // 1) request succeeds or 2) it fails or never happens, in which case we
837
+ // need to retry this segment.
829
838
this . _maybeSaveSession ( ) ;
830
839
831
- await sendReplay ( {
840
+ let recordingData : ReplayRecordingData ;
841
+
842
+ if ( options . finishImmediate && this . eventBuffer . pendingLength ) {
843
+ recordingData = this . eventBuffer . finishImmediate ( ) ;
844
+ } else {
845
+ // NOTE: Be mindful that nothing after this point (the first `await`)
846
+ // will run after when the page is unloaded.
847
+ await Promise . all ( promises ) ;
848
+
849
+ // This can be empty due to blur events calling `runFlush` directly. In
850
+ // the case where we have a snapshot checkout and a blur event
851
+ // happening near the same time, the blur event can end up emptying the
852
+ // buffer even if snapshot happens first.
853
+ if ( ! this . eventBuffer . pendingLength ) {
854
+ return ;
855
+ }
856
+ // This empties the event buffer regardless of outcome of sending replay
857
+ recordingData = await this . eventBuffer . finish ( ) ;
858
+ }
859
+
860
+ const sendReplayPromise = sendReplay ( {
832
861
replayId,
833
862
recordingData,
834
863
segmentId,
@@ -838,6 +867,10 @@ export class ReplayContainer implements ReplayContainerInterface {
838
867
options : this . getOptions ( ) ,
839
868
timestamp : new Date ( ) . getTime ( ) ,
840
869
} ) ;
870
+
871
+ await sendReplayPromise ;
872
+
873
+ return ;
841
874
} catch ( err ) {
842
875
this . _handleException ( err ) ;
843
876
0 commit comments