@@ -38,17 +38,21 @@ import type {
38
38
Session ,
39
39
WorkerAddEventResponse ,
40
40
} from './types' ;
41
+ import { FlushState } from './types' ;
41
42
import { addEvent } from './util/addEvent' ;
42
43
import { addMemoryEntry } from './util/addMemoryEntry' ;
44
+ import { clearPendingReplay } from './util/clearPendingReplay' ;
43
45
import { createBreadcrumb } from './util/createBreadcrumb' ;
44
46
import { createPerformanceSpans } from './util/createPerformanceSpans' ;
45
47
import { createRecordingData } from './util/createRecordingData' ;
46
48
import { createReplayEnvelope } from './util/createReplayEnvelope' ;
47
49
import { debounce } from './util/debounce' ;
50
+ import { getPendingReplay } from './util/getPendingReplay' ;
48
51
import { isExpired } from './util/isExpired' ;
49
52
import { isSessionExpired } from './util/isSessionExpired' ;
50
53
import { overwriteRecordDroppedEvent , restoreRecordDroppedEvent } from './util/monkeyPatchRecordDroppedEvent' ;
51
54
import { prepareReplayEvent } from './util/prepareReplayEvent' ;
55
+ import { setFlushState } from './util/setFlushState' ;
52
56
53
57
/**
54
58
* Returns true to return control to calling function, otherwise continue with normal batching
@@ -164,7 +168,7 @@ export class ReplayContainer implements ReplayContainerInterface {
164
168
* Creates or loads a session, attaches listeners to varying events (DOM,
165
169
* _performanceObserver, Recording, Sentry SDK, etc)
166
170
*/
167
- start ( ) : void {
171
+ async start ( ) : Promise < void > {
168
172
this . setInitialState ( ) ;
169
173
170
174
this . loadSession ( { expiry : SESSION_IDLE_DURATION } ) ;
@@ -175,6 +179,19 @@ export class ReplayContainer implements ReplayContainerInterface {
175
179
return ;
176
180
}
177
181
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
+
178
195
if ( ! this . session . sampled ) {
179
196
// If session was not sampled, then we do not initialize the integration at all.
180
197
return ;
@@ -193,7 +210,7 @@ export class ReplayContainer implements ReplayContainerInterface {
193
210
this . updateSessionActivity ( ) ;
194
211
195
212
this . eventBuffer = createEventBuffer ( {
196
- useCompression : Boolean ( this . _options . useCompression ) ,
213
+ useCompression,
197
214
} ) ;
198
215
199
216
this . addListeners ( ) ;
@@ -318,6 +335,7 @@ export class ReplayContainer implements ReplayContainerInterface {
318
335
// enable flag to create the root replay
319
336
if ( type === 'new' ) {
320
337
this . setInitialState ( ) ;
338
+ clearPendingReplay ( ) ;
321
339
}
322
340
323
341
if ( session . id !== this . session ?. id ) {
@@ -805,36 +823,61 @@ export class ReplayContainer implements ReplayContainerInterface {
805
823
return ;
806
824
}
807
825
808
- await this . addPerformanceEntries ( ) ;
826
+ try {
827
+ const promises : Promise < any > [ ] = [ ] ;
809
828
810
- if ( ! this . eventBuffer ?. length ) {
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
+ }
813
835
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 ) ) ;
816
838
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 ( ) ) ;
820
841
821
842
// NOTE: Copy values from instance members, as it's possible they could
822
843
// change before the flush finishes.
823
844
const replayId = this . session . id ;
824
845
const eventContext = this . popEventContext ( ) ;
825
846
// Always increment segmentId regardless of outcome of sending replay
826
847
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
827
860
this . _maybeSaveSession ( ) ;
828
861
829
- await this . sendReplay ( {
862
+ const [ , , recordingData ] = await Promise . all ( promises ) ;
863
+
864
+ const sendReplayPromise = this . sendReplay ( {
830
865
replayId,
831
866
events : recordingData ,
832
867
segmentId,
833
868
includeReplayStartTimestamp : segmentId === 0 ,
834
869
eventContext,
835
870
} ) ;
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 ) ;
836
878
} catch ( err ) {
837
879
this . handleException ( err ) ;
880
+ setFlushState ( FlushState . ERROR ) ;
838
881
}
839
882
}
840
883
@@ -956,6 +999,10 @@ export class ReplayContainer implements ReplayContainerInterface {
956
999
errorSampleRate : this . _options . errorSampleRate ,
957
1000
} ;
958
1001
1002
+ // Replays have separate set of breadcrumbs, do not include breadcrumbs
1003
+ // from core SDK
1004
+ delete replayEvent . breadcrumbs ;
1005
+
959
1006
/*
960
1007
For reference, the fully built event looks something like this:
961
1008
{
0 commit comments