@@ -38,17 +38,21 @@ import type {
3838 Session ,
3939 WorkerAddEventResponse ,
4040} from './types' ;
41+ import { FlushState } from './types' ;
4142import { addEvent } from './util/addEvent' ;
4243import { addMemoryEntry } from './util/addMemoryEntry' ;
44+ import { clearPendingReplay } from './util/clearPendingReplay' ;
4345import { createBreadcrumb } from './util/createBreadcrumb' ;
4446import { createPerformanceSpans } from './util/createPerformanceSpans' ;
4547import { createRecordingData } from './util/createRecordingData' ;
4648import { createReplayEnvelope } from './util/createReplayEnvelope' ;
4749import { debounce } from './util/debounce' ;
50+ import { getPendingReplay } from './util/getPendingReplay' ;
4851import { isExpired } from './util/isExpired' ;
4952import { isSessionExpired } from './util/isSessionExpired' ;
5053import { overwriteRecordDroppedEvent , restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent' ;
5154import { prepareReplayEvent } from './util/prepareReplayEvent' ;
55+ import { setFlushState } from './util/setFlushState' ;
5256
5357/**
5458 * Returns true to return control to calling function, otherwise continue with normal batching
@@ -164,7 +168,7 @@ export class ReplayContainer implements ReplayContainerInterface {
164168 * Creates or loads a session, attaches listeners to varying events (DOM,
165169 * _performanceObserver, Recording, Sentry SDK, etc)
166170 */
167- start ( ) : void {
171+ async start ( ) : Promise < void > {
168172 this . setInitialState ( ) ;
169173
170174 this . loadSession ( { expiry : SESSION_IDLE_DURATION } ) ;
@@ -175,6 +179,19 @@ export class ReplayContainer implements ReplayContainerInterface {
175179 return ;
176180 }
177181
182+ const useCompression = Boolean ( this . _options . useCompression ) ;
183+
184+ // Flush any pending events that were previously unable to be sent
185+ try {
186+ const pendingEvent = await getPendingReplay ( { useCompression } ) ;
187+ if ( pendingEvent ) {
188+ await this . sendReplayRequest ( pendingEvent ) ;
189+ clearPendingReplay ( ) ;
190+ }
191+ } catch {
192+ // ignore
193+ }
194+
178195 if ( ! this . session . sampled ) {
179196 // If session was not sampled, then we do not initialize the integration at all.
180197 return ;
@@ -193,7 +210,7 @@ export class ReplayContainer implements ReplayContainerInterface {
193210 this . updateSessionActivity ( ) ;
194211
195212 this . eventBuffer = createEventBuffer ( {
196- useCompression : Boolean ( this . _options . useCompression ) ,
213+ useCompression,
197214 } ) ;
198215
199216 this . addListeners ( ) ;
@@ -318,6 +335,7 @@ export class ReplayContainer implements ReplayContainerInterface {
318335 // enable flag to create the root replay
319336 if ( type === 'new' ) {
320337 this . setInitialState ( ) ;
338+ clearPendingReplay ( ) ;
321339 }
322340
323341 if ( session . id !== this . session ?. id ) {
@@ -805,36 +823,61 @@ export class ReplayContainer implements ReplayContainerInterface {
805823 return ;
806824 }
807825
808- await this . addPerformanceEntries ( ) ;
826+ try {
827+ const promises : Promise < any > [ ] = [ ] ;
809828
810- if ( ! this . eventBuffer ?. pendingLength ) {
811- return ;
812- }
829+ promises . push ( this . addPerformanceEntries ( ) ) ;
830+
831+ // Do not continue if there are no pending events in buffer
832+ if ( ! this . eventBuffer ?. pendingLength ) {
833+ return ;
834+ }
813835
814- // Only attach memory event if eventBuffer is not empty
815- await addMemoryEntry ( this ) ;
836+ // Only attach memory entry if eventBuffer is not empty
837+ promises . push ( addMemoryEntry ( this ) ) ;
816838
817- try {
818- // Note this empties the event buffer regardless of outcome of sending replay
819- const recordingData = await this . eventBuffer . finish ( ) ;
839+ // This empties the event buffer regardless of outcome of sending replay
840+ promises . push ( this . eventBuffer . finish ( ) ) ;
820841
821842 // NOTE: Copy values from instance members, as it's possible they could
822843 // change before the flush finishes.
823844 const replayId = this . session . id ;
824845 const eventContext = this . popEventContext ( ) ;
825846 // Always increment segmentId regardless of outcome of sending replay
826847 const segmentId = this . session . segmentId ++ ;
848+
849+ // Write to local storage before flushing, in case flush request never starts
850+ setFlushState ( FlushState . START , {
851+ events : this . eventBuffer . pendingEvents ,
852+ replayId,
853+ eventContext,
854+ segmentId,
855+ includeReplayStartTimestamp : segmentId === 0 ,
856+ timestamp : new Date ( ) . getTime ( ) ,
857+ } ) ;
858+
859+ // Save session (new segment id) after we save flush data assuming
827860 this . _maybeSaveSession ( ) ;
828861
829- await this . sendReplay ( {
862+ const [ , , recordingData ] = await Promise . all ( promises ) ;
863+
864+ const sendReplayPromise = this . sendReplay ( {
830865 replayId,
831866 events : recordingData ,
832867 segmentId,
833868 includeReplayStartTimestamp : segmentId === 0 ,
834869 eventContext,
835870 } ) ;
871+
872+ // If replay request starts, optimistically update some states
873+ setFlushState ( FlushState . SENT_REQUEST ) ;
874+
875+ await sendReplayPromise ;
876+
877+ setFlushState ( FlushState . SENT_REQUEST ) ;
836878 } catch ( err ) {
837879 this . handleException ( err ) ;
880+ setFlushState ( FlushState . ERROR ) ;
838881 }
839882 }
840883
@@ -956,6 +999,10 @@ export class ReplayContainer implements ReplayContainerInterface {
956999 errorSampleRate : this . _options . errorSampleRate ,
9571000 } ;
9581001
1002+ // Replays have separate set of breadcrumbs, do not include breadcrumbs
1003+ // from core SDK
1004+ delete replayEvent . breadcrumbs ;
1005+
9591006 /*
9601007 For reference, the fully built event looks something like this:
9611008 {
0 commit comments