@@ -36,18 +36,22 @@ import type {
36
36
SendReplay ,
37
37
Session ,
38
38
} from './types' ;
39
+ import { FlushState } from './types' ;
39
40
import { addEvent } from './util/addEvent' ;
40
41
import { addMemoryEntry } from './util/addMemoryEntry' ;
42
+ import { clearPendingReplay } from './util/clearPendingReplay' ;
41
43
import { createBreadcrumb } from './util/createBreadcrumb' ;
42
44
import { createPerformanceEntries } from './util/createPerformanceEntries' ;
43
45
import { createPerformanceSpans } from './util/createPerformanceSpans' ;
44
46
import { createRecordingData } from './util/createRecordingData' ;
45
47
import { createReplayEnvelope } from './util/createReplayEnvelope' ;
46
48
import { debounce } from './util/debounce' ;
49
+ import { getPendingReplay } from './util/getPendingReplay' ;
47
50
import { isExpired } from './util/isExpired' ;
48
51
import { isSessionExpired } from './util/isSessionExpired' ;
49
52
import { overwriteRecordDroppedEvent , restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent' ;
50
53
import { prepareReplayEvent } from './util/prepareReplayEvent' ;
54
+ import { setFlushState } from './util/setFlushState' ;
51
55
52
56
/**
53
57
* Returns true to return control to calling function, otherwise continue with normal batching
@@ -163,7 +167,7 @@ export class ReplayContainer implements ReplayContainerInterface {
163
167
* Creates or loads a session, attaches listeners to varying events (DOM,
164
168
* _performanceObserver, Recording, Sentry SDK, etc)
165
169
*/
166
- start ( ) : void {
170
+ async start ( ) : Promise < void > {
167
171
this . setInitialState ( ) ;
168
172
169
173
this . loadSession ( { expiry : SESSION_IDLE_DURATION } ) ;
@@ -174,6 +178,19 @@ export class ReplayContainer implements ReplayContainerInterface {
174
178
return ;
175
179
}
176
180
181
+ const useCompression = Boolean ( this . _options . useCompression ) ;
182
+
183
+ // Flush any pending events that were previously unable to be sent
184
+ try {
185
+ const pendingEvent = await getPendingReplay ( { useCompression } ) ;
186
+ if ( pendingEvent ) {
187
+ await this . sendReplayRequest ( pendingEvent ) ;
188
+ clearPendingReplay ( ) ;
189
+ }
190
+ } catch {
191
+ // ignore
192
+ }
193
+
177
194
if ( ! this . session . sampled ) {
178
195
// If session was not sampled, then we do not initialize the integration at all.
179
196
return ;
@@ -190,7 +207,7 @@ export class ReplayContainer implements ReplayContainerInterface {
190
207
this . updateSessionActivity ( ) ;
191
208
192
209
this . eventBuffer = createEventBuffer ( {
193
- useCompression : Boolean ( this . _options . useCompression ) ,
210
+ useCompression,
194
211
} ) ;
195
212
196
213
this . addListeners ( ) ;
@@ -305,6 +322,7 @@ export class ReplayContainer implements ReplayContainerInterface {
305
322
// enable flag to create the root replay
306
323
if ( type === 'new' ) {
307
324
this . setInitialState ( ) ;
325
+ clearPendingReplay ( ) ;
308
326
}
309
327
310
328
if ( session . id !== this . session ?. id ) {
@@ -792,36 +810,61 @@ export class ReplayContainer implements ReplayContainerInterface {
792
810
return ;
793
811
}
794
812
795
- await this . addPerformanceEntries ( ) ;
813
+ try {
814
+ const promises : Promise < any > [ ] = [ ] ;
796
815
797
- if ( ! this . eventBuffer ?. pendingLength ) {
798
- return ;
799
- }
816
+ promises . push ( this . addPerformanceEntries ( ) ) ;
817
+
818
+ // Do not continue if there are no pending events in buffer
819
+ if ( ! this . eventBuffer ?. pendingLength ) {
820
+ return ;
821
+ }
800
822
801
- // Only attach memory event if eventBuffer is not empty
802
- await addMemoryEntry ( this ) ;
823
+ // Only attach memory entry if eventBuffer is not empty
824
+ promises . push ( addMemoryEntry ( this ) ) ;
803
825
804
- try {
805
- // Note this empties the event buffer regardless of outcome of sending replay
806
- const recordingData = await this . eventBuffer . finish ( ) ;
826
+ // This empties the event buffer regardless of outcome of sending replay
827
+ promises . push ( this . eventBuffer . finish ( ) ) ;
807
828
808
829
// NOTE: Copy values from instance members, as it's possible they could
809
830
// change before the flush finishes.
810
831
const replayId = this . session . id ;
811
832
const eventContext = this . popEventContext ( ) ;
812
833
// Always increment segmentId regardless of outcome of sending replay
813
834
const segmentId = this . session . segmentId ++ ;
835
+
836
+ // Write to local storage before flushing, in case flush request never starts
837
+ setFlushState ( FlushState . START , {
838
+ events : this . eventBuffer . pendingEvents ,
839
+ replayId,
840
+ eventContext,
841
+ segmentId,
842
+ includeReplayStartTimestamp : segmentId === 0 ,
843
+ timestamp : new Date ( ) . getTime ( ) ,
844
+ } ) ;
845
+
846
+ // Save session (new segment id) after we save flush data assuming
814
847
this . _maybeSaveSession ( ) ;
815
848
816
- await this . sendReplay ( {
849
+ const [ , , recordingData ] = await Promise . all ( promises ) ;
850
+
851
+ const sendReplayPromise = this . sendReplay ( {
817
852
replayId,
818
853
events : recordingData ,
819
854
segmentId,
820
855
includeReplayStartTimestamp : segmentId === 0 ,
821
856
eventContext,
822
857
} ) ;
858
+
859
+ // If replay request starts, optimistically update some states
860
+ setFlushState ( FlushState . SENT_REQUEST ) ;
861
+
862
+ await sendReplayPromise ;
863
+
864
+ setFlushState ( FlushState . SENT_REQUEST ) ;
823
865
} catch ( err ) {
824
866
this . handleException ( err ) ;
867
+ setFlushState ( FlushState . ERROR ) ;
825
868
}
826
869
}
827
870
@@ -943,6 +986,10 @@ export class ReplayContainer implements ReplayContainerInterface {
943
986
errorSampleRate : this . _options . errorSampleRate ,
944
987
} ;
945
988
989
+ // Replays have separate set of breadcrumbs, do not include breadcrumbs
990
+ // from core SDK
991
+ delete replayEvent . breadcrumbs ;
992
+
946
993
/*
947
994
For reference, the fully built event looks something like this:
948
995
{
0 commit comments