11/* eslint-disable max-lines */ // TODO: We might want to split this file up
22import { addGlobalEventProcessor , captureException , getCurrentHub } from '@sentry/core' ;
3- import type { Breadcrumb , ReplayRecordingMode } from '@sentry/types' ;
3+ import type { Breadcrumb , ReplayRecordingMode , ReplayRecordingData } from '@sentry/types' ;
44import type { RateLimits } from '@sentry/utils' ;
55import { addInstrumentationHandler , disabledUntil , logger } from '@sentry/utils' ;
66import { EventType , record } from 'rrweb' ;
@@ -28,6 +28,7 @@ import type {
2828 ReplayContainer as ReplayContainerInterface ,
2929 ReplayPluginOptions ,
3030 Session ,
31+ FlushOptions ,
3132} from './types' ;
3233import { addEvent } from './util/addEvent' ;
3334import { addMemoryEntry } from './util/addMemoryEntry' ;
@@ -151,7 +152,7 @@ export class ReplayContainer implements ReplayContainerInterface {
151152 * Creates or loads a session, attaches listeners to varying events (DOM,
152153 * _performanceObserver, Recording, Sentry SDK, etc)
153154 */
154- public start ( ) : void {
155+ public async start ( ) : Promise < void > {
155156 this . _setInitialState ( ) ;
156157
157158 this . _loadSession ( { expiry : SESSION_IDLE_DURATION } ) ;
@@ -324,7 +325,6 @@ export class ReplayContainer implements ReplayContainerInterface {
324325 }
325326
326327 /**
327- *
328328 * Always flush via `_debouncedFlush` so that we do not have flushes triggered
329329 * from calling both `flush` and `_debouncedFlush`. Otherwise, there could be
330330 * cases of mulitple flushes happening closely together.
@@ -335,7 +335,7 @@ export class ReplayContainer implements ReplayContainerInterface {
335335 return this . _debouncedFlush . flush ( ) as Promise < void > ;
336336 }
337337
338- /** Get the current sesion (=replay) ID */
338+ /** Get the current session (=replay) ID */
339339 public getSessionId ( ) : string | undefined {
340340 return this . session && this . session . id ;
341341 }
@@ -625,7 +625,7 @@ export class ReplayContainer implements ReplayContainerInterface {
625625 // Send replay when the page/tab becomes hidden. There is no reason to send
626626 // replay if it becomes visible, since no actions we care about were done
627627 // while it was hidden
628- this . _conditionalFlush ( ) ;
628+ this . _conditionalFlush ( { finishImmediate : true } ) ;
629629 }
630630
631631 /**
@@ -747,11 +747,20 @@ export class ReplayContainer implements ReplayContainerInterface {
747747 /**
748748 * Only flush if `this.recordingMode === 'session'`
749749 */
750- private _conditionalFlush ( ) : void {
750+ private _conditionalFlush ( options : FlushOptions = { } ) : void {
751751 if ( this . recordingMode === 'error' ) {
752752 return ;
753753 }
754754
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+
755764 void this . flushImmediate ( ) ;
756765 }
757766
@@ -795,40 +804,60 @@ export class ReplayContainer implements ReplayContainerInterface {
795804 *
796805 * Should never be called directly, only by `flush`
797806 */
798- private async _runFlush ( ) : Promise < void > {
807+ private async _runFlush ( options : FlushOptions = { } ) : Promise < void > {
799808 if ( ! this . session || ! this . eventBuffer ) {
800809 __DEBUG_BUILD__ && logger . error ( '[Replay] No session or eventBuffer found to flush.' ) ;
801810 return ;
802811 }
803812
804- await this . _addPerformanceEntries ( ) ;
813+ try {
814+ this . _debouncedFlush . cancel ( ) ;
805815
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 > [ ] = [ ] ;
810817
811- // Only attach memory event if eventBuffer is not empty
812- await addMemoryEntry ( this ) ;
818+ promises . push ( this . _addPerformanceEntries ( ) ) ;
813819
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+ }
818824
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 ) ) ;
822827
823828 // NOTE: Copy values from instance members, as it's possible they could
824829 // change before the flush finishes.
825830 const replayId = this . session . id ;
826831 const eventContext = this . _popEventContext ( ) ;
827832 // Always increment segmentId regardless of outcome of sending replay
828833 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.
829838 this . _maybeSaveSession ( ) ;
830839
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 ( {
832861 replayId,
833862 recordingData,
834863 segmentId,
@@ -838,6 +867,10 @@ export class ReplayContainer implements ReplayContainerInterface {
838867 options : this . getOptions ( ) ,
839868 timestamp : new Date ( ) . getTime ( ) ,
840869 } ) ;
870+
871+ await sendReplayPromise ;
872+
873+ return ;
841874 } catch ( err ) {
842875 this . _handleException ( err ) ;
843876
0 commit comments