@@ -18,8 +18,8 @@ import { setupPerformanceObserver } from './coreHandlers/performanceObserver';
1818import { createEventBuffer } from './eventBuffer' ;
1919import { clearSession } from './session/clearSession' ;
2020import { loadOrCreateSession } from './session/loadOrCreateSession' ;
21- import { maybeRefreshSession } from './session/maybeRefreshSession' ;
2221import { saveSession } from './session/saveSession' ;
22+ import { shouldRefreshSession } from './session/shouldRefreshSession' ;
2323import type {
2424 AddEventResult ,
2525 AddUpdateCallback ,
@@ -217,7 +217,7 @@ export class ReplayContainer implements ReplayContainerInterface {
217217 * Initializes the plugin based on sampling configuration. Should not be
218218 * called outside of constructor.
219219 */
220- public initializeSampling ( ) : void {
220+ public initializeSampling ( previousSessionId ?: string ) : void {
221221 const { errorSampleRate, sessionSampleRate } = this . _options ;
222222
223223 // If neither sample rate is > 0, then do nothing - user will need to call one of
@@ -228,7 +228,7 @@ export class ReplayContainer implements ReplayContainerInterface {
228228
229229 // Otherwise if there is _any_ sample rate set, try to load an existing
230230 // session, or create a new one.
231- this . _initializeSessionForSampling ( ) ;
231+ this . _initializeSessionForSampling ( previousSessionId ) ;
232232
233233 if ( ! this . session ) {
234234 // This should not happen, something wrong has occurred
@@ -273,7 +273,6 @@ export class ReplayContainer implements ReplayContainerInterface {
273273 logInfoNextTick ( '[Replay] Starting replay in session mode' , this . _options . _experiments . traceInternals ) ;
274274
275275 const session = loadOrCreateSession (
276- this . session ,
277276 {
278277 maxReplayDuration : this . _options . maxReplayDuration ,
279278 sessionIdleExpire : this . timeouts . sessionIdleExpire ,
@@ -304,7 +303,6 @@ export class ReplayContainer implements ReplayContainerInterface {
304303 logInfoNextTick ( '[Replay] Starting replay in buffer mode' , this . _options . _experiments . traceInternals ) ;
305304
306305 const session = loadOrCreateSession (
307- this . session ,
308306 {
309307 sessionIdleExpire : this . timeouts . sessionIdleExpire ,
310308 maxReplayDuration : this . _options . maxReplayDuration ,
@@ -373,15 +371,16 @@ export class ReplayContainer implements ReplayContainerInterface {
373371 return ;
374372 }
375373
374+ // We can't move `_isEnabled` after awaiting a flush, otherwise we can
375+ // enter into an infinite loop when `stop()` is called while flushing.
376+ this . _isEnabled = false ;
377+
376378 try {
377379 logInfo (
378380 `[Replay] Stopping Replay${ reason ? ` triggered by ${ reason } ` : '' } ` ,
379381 this . _options . _experiments . traceInternals ,
380382 ) ;
381383
382- // We can't move `_isEnabled` after awaiting a flush, otherwise we can
383- // enter into an infinite loop when `stop()` is called while flushing.
384- this . _isEnabled = false ;
385384 this . _removeListeners ( ) ;
386385 this . stopRecording ( ) ;
387386
@@ -475,16 +474,6 @@ export class ReplayContainer implements ReplayContainerInterface {
475474
476475 // Once this session ends, we do not want to refresh it
477476 if ( this . session ) {
478- this . session . shouldRefresh = false ;
479-
480- // It's possible that the session lifespan is > max session lifespan
481- // because we have been buffering beyond max session lifespan (we ignore
482- // expiration given that `shouldRefresh` is true). Since we flip
483- // `shouldRefresh`, the session could be considered expired due to
484- // lifespan, which is not what we want. Update session start date to be
485- // the current timestamp, so that session is not considered to be
486- // expired. This means that max replay duration can be MAX_REPLAY_DURATION +
487- // (length of buffer), which we are ok with.
488477 this . _updateUserActivity ( activityTime ) ;
489478 this . _updateSessionActivity ( activityTime ) ;
490479 this . _maybeSaveSession ( ) ;
@@ -612,8 +601,6 @@ export class ReplayContainer implements ReplayContainerInterface {
612601 * @hidden
613602 */
614603 public checkAndHandleExpiredSession ( ) : boolean | void {
615- const oldSessionId = this . getSessionId ( ) ;
616-
617604 // Prevent starting a new session if the last user activity is older than
618605 // SESSION_IDLE_PAUSE_DURATION. Otherwise non-user activity can trigger a new
619606 // session+recording. This creates noisy replays that do not have much
@@ -635,24 +622,11 @@ export class ReplayContainer implements ReplayContainerInterface {
635622 // --- There is recent user activity --- //
636623 // This will create a new session if expired, based on expiry length
637624 if ( ! this . _checkSession ( ) ) {
638- return ;
639- }
640-
641- // Session was expired if session ids do not match
642- const expired = oldSessionId !== this . getSessionId ( ) ;
643-
644- if ( ! expired ) {
645- return true ;
646- }
647-
648- // Session is expired, trigger a full snapshot (which will create a new session)
649- if ( this . isPaused ( ) ) {
650- this . resume ( ) ;
651- } else {
652- this . _triggerFullSnapshot ( ) ;
625+ // Check session handles the refreshing itself
626+ return false ;
653627 }
654628
655- return false ;
629+ return true ;
656630 }
657631
658632 /**
@@ -740,6 +714,7 @@ export class ReplayContainer implements ReplayContainerInterface {
740714
741715 // Need to set as enabled before we start recording, as `record()` can trigger a flush with a new checkout
742716 this . _isEnabled = true ;
717+ this . _isPaused = false ;
743718
744719 this . startRecording ( ) ;
745720 }
@@ -756,17 +731,17 @@ export class ReplayContainer implements ReplayContainerInterface {
756731 /**
757732 * Loads (or refreshes) the current session.
758733 */
759- private _initializeSessionForSampling ( ) : void {
734+ private _initializeSessionForSampling ( previousSessionId ?: string ) : void {
760735 // Whenever there is _any_ error sample rate, we always allow buffering
761736 // Because we decide on sampling when an error occurs, we need to buffer at all times if sampling for errors
762737 const allowBuffering = this . _options . errorSampleRate > 0 ;
763738
764739 const session = loadOrCreateSession (
765- this . session ,
766740 {
767741 sessionIdleExpire : this . timeouts . sessionIdleExpire ,
768742 maxReplayDuration : this . _options . maxReplayDuration ,
769743 traceInternals : this . _options . _experiments . traceInternals ,
744+ previousSessionId,
770745 } ,
771746 {
772747 stickySession : this . _options . stickySession ,
@@ -791,37 +766,32 @@ export class ReplayContainer implements ReplayContainerInterface {
791766
792767 const currentSession = this . session ;
793768
794- const newSession = maybeRefreshSession (
795- currentSession ,
796- {
769+ if (
770+ shouldRefreshSession ( currentSession , {
797771 sessionIdleExpire : this . timeouts . sessionIdleExpire ,
798- traceInternals : this . _options . _experiments . traceInternals ,
799772 maxReplayDuration : this . _options . maxReplayDuration ,
800- } ,
801- {
802- stickySession : Boolean ( this . _options . stickySession ) ,
803- sessionSampleRate : this . _options . sessionSampleRate ,
804- allowBuffering : this . _options . errorSampleRate > 0 ,
805- } ,
806- ) ;
807-
808- const isNew = newSession . id !== currentSession . id ;
809-
810- // If session was newly created (i.e. was not loaded from storage), then
811- // enable flag to create the root replay
812- if ( isNew ) {
813- this . setInitialState ( ) ;
814- this . session = newSession ;
815- }
816-
817- if ( ! this . session . sampled ) {
818- void this . stop ( { reason : 'session not refreshed' } ) ;
773+ } )
774+ ) {
775+ void this . _refreshSession ( currentSession ) ;
819776 return false ;
820777 }
821778
822779 return true ;
823780 }
824781
782+ /**
783+ * Refresh a session with a new one.
784+ * This stops the current session (without forcing a flush, as that would never work since we are expired),
785+ * and then does a new sampling based on the refreshed session.
786+ */
787+ private async _refreshSession ( session : Session ) : Promise < void > {
788+ if ( ! this . _isEnabled ) {
789+ return ;
790+ }
791+ await this . stop ( { reason : 'refresh session' } ) ;
792+ this . initializeSampling ( session . id ) ;
793+ }
794+
825795 /**
826796 * Adds listeners to record events for the replay
827797 */
@@ -933,10 +903,14 @@ export class ReplayContainer implements ReplayContainerInterface {
933903
934904 const expired = isSessionExpired ( this . session , {
935905 maxReplayDuration : this . _options . maxReplayDuration ,
936- ... this . timeouts ,
906+ sessionIdleExpire : this . timeouts . sessionIdleExpire ,
937907 } ) ;
938908
939- if ( breadcrumb && ! expired ) {
909+ if ( expired ) {
910+ return ;
911+ }
912+
913+ if ( breadcrumb ) {
940914 this . _createCustomBreadcrumb ( breadcrumb ) ;
941915 }
942916
@@ -1081,7 +1055,9 @@ export class ReplayContainer implements ReplayContainerInterface {
10811055 * Should never be called directly, only by `flush`
10821056 */
10831057 private async _runFlush ( ) : Promise < void > {
1084- if ( ! this . session || ! this . eventBuffer ) {
1058+ const replayId = this . getSessionId ( ) ;
1059+
1060+ if ( ! this . session || ! this . eventBuffer || ! replayId ) {
10851061 __DEBUG_BUILD__ && logger . error ( '[Replay] No session or eventBuffer found to flush.' ) ;
10861062 return ;
10871063 }
@@ -1101,13 +1077,15 @@ export class ReplayContainer implements ReplayContainerInterface {
11011077 return ;
11021078 }
11031079
1080+ // if this changed in the meanwhile, e.g. because the session was refreshed or similar, we abort here
1081+ if ( replayId !== this . getSessionId ( ) ) {
1082+ return ;
1083+ }
1084+
11041085 try {
11051086 // This uses the data from the eventBuffer, so we need to call this before `finish()
11061087 this . _updateInitialTimestampFromEventBuffer ( ) ;
11071088
1108- // Note this empties the event buffer regardless of outcome of sending replay
1109- const recordingData = await this . eventBuffer . finish ( ) ;
1110-
11111089 const timestamp = Date . now ( ) ;
11121090
11131091 // Check total duration again, to avoid sending outdated stuff
@@ -1117,14 +1095,14 @@ export class ReplayContainer implements ReplayContainerInterface {
11171095 throw new Error ( 'Session is too long, not sending replay' ) ;
11181096 }
11191097
1120- // NOTE: Copy values from instance members, as it's possible they could
1121- // change before the flush finishes.
1122- const replayId = this . session . id ;
11231098 const eventContext = this . _popEventContext ( ) ;
11241099 // Always increment segmentId regardless of outcome of sending replay
11251100 const segmentId = this . session . segmentId ++ ;
11261101 this . _maybeSaveSession ( ) ;
11271102
1103+ // Note this empties the event buffer regardless of outcome of sending replay
1104+ const recordingData = await this . eventBuffer . finish ( ) ;
1105+
11281106 await sendReplay ( {
11291107 replayId,
11301108 recordingData,
0 commit comments