@oneuptime/react-native-replay 14.0.11 → 14.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -90,6 +90,7 @@ var SESSION_REPLAY_CHECKOUT_INTERVAL_MS = 6e4;
90
90
  var SESSION_REPLAY_ROLLING_BUFFER_MS = 6e4;
91
91
  var SESSION_REPLAY_ROLLING_BUFFER_BYTES = 2 * 1024 * 1024;
92
92
  var SESSION_REPLAY_IDLE_ROLLOVER_MS = 30 * 6e4;
93
+ var SESSION_REPLAY_IDLE_PAUSE_MS = 5 * 6e4;
93
94
  var SESSION_REPLAY_MAX_SESSION_MS = 4 * 60 * 6e4;
94
95
  var SESSION_REPLAY_MAX_DECOMPRESSED_FRAME_BYTES = 8 * 1024 * 1024;
95
96
  var MAX_SESSION_REPLAY_CHUNK_BYTES = 2 * 1024 * 1024;
@@ -98,7 +99,7 @@ var SESSION_REPLAY_MAX_OFFLINE_DELAY_MS = 3 * 24 * 60 * 60 * 1e3;
98
99
  var MOBILE_CAPTURE_INTERVAL_MS = 500;
99
100
  var MOBILE_RECORDER_KIND = "rn-view-tree";
100
101
  var SYNTHETIC_RRWEB_VERSION = "synthetic-1";
101
- var RECORDER_VERSION = true ? "14.0.11" : "0.0.0-dev";
102
+ var RECORDER_VERSION = true ? "14.0.13" : "0.0.0-dev";
102
103
  var SESSION_REPLAY_MOBILE_RECORDER_CAPABILITIES = [
103
104
  "mobile-view-tree",
104
105
  "mobile-touch-events",
@@ -150,6 +151,9 @@ var IDENTIFY_CUSTOM_EVENT_TAG = "oneuptime.identify";
150
151
  var TAGS_CUSTOM_EVENT_TAG = "oneuptime.tags";
151
152
  var VISIBILITY_CUSTOM_EVENT_TAG = "oneuptime.visibility";
152
153
  var TOUCH_CUSTOM_EVENT_TAG = "oneuptime.touch";
154
+ var SESSION_ROTATED_CUSTOM_EVENT_TAG = "oneuptime.session-rotated";
155
+ var IDLE_PAUSED_CUSTOM_EVENT_TAG = "oneuptime.idle-paused";
156
+ var IDLE_RESUMED_CUSTOM_EVENT_TAG = "oneuptime.idle-resumed";
153
157
 
154
158
  // src/Sanitize.ts
155
159
  var MAX_TRAIT_KEYS = 20;
@@ -2525,6 +2529,18 @@ var ReplaySessionStore = class {
2525
2529
  };
2526
2530
  return { ...this.identity };
2527
2531
  }
2532
+ /*
2533
+ * Forget the stored session without starting the next one. A session
2534
+ * that ends while the recorder is paused for idle has no successor until
2535
+ * the person comes back, and its record - last written by its own final
2536
+ * chunk, so it looks recent - would otherwise let a relaunch in the
2537
+ * meantime carry on a session that is already over. The visitor id stays,
2538
+ * and so does the in-memory identity: nothing more is sent under it, and
2539
+ * the next rotate() writes the new session.
2540
+ */
2541
+ async end() {
2542
+ await this.safeRemove(this.sessionKey);
2543
+ }
2528
2544
  async clear() {
2529
2545
  this.identity = null;
2530
2546
  await Promise.all([
@@ -2983,6 +2999,7 @@ function isTouchPrivate(privacyMap, x2, y) {
2983
2999
  var MAX_DIAGNOSTIC_EVENTS = 100;
2984
3000
  var TOUCH_MOVE_SAMPLE_MS = 50;
2985
3001
  var MOBILE_POLICY_REFRESH_INTERVAL_MS = 5 * 6e4;
3002
+ var MAX_EVENTS_HELD_FOR_RESUME = 500;
2986
3003
  var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
2987
3004
  "AggregateError",
2988
3005
  "Error",
@@ -3054,6 +3071,7 @@ var MobileReplayRecorder = class {
3054
3071
  __publicField(this, "storage");
3055
3072
  __publicField(this, "nativeViewTree");
3056
3073
  __publicField(this, "appState");
3074
+ __publicField(this, "keyboard");
3057
3075
  __publicField(this, "now");
3058
3076
  __publicField(this, "serializer", new ViewTreeSerializer());
3059
3077
  __publicField(this, "chunkBuffer", new ReplayChunkBuffer());
@@ -3120,6 +3138,21 @@ var MobileReplayRecorder = class {
3120
3138
  __publicField(this, "pendingStartCancellation", null);
3121
3139
  __publicField(this, "identityPersisted", false);
3122
3140
  __publicField(this, "sessionLastActivityUnixMs", 0);
3141
+ /*
3142
+ * The session went the idle window without activity, was sealed where it
3143
+ * ended, and nobody has come back since.
3144
+ *
3145
+ * Rotating on the spot instead (the old behaviour) started a session
3146
+ * nobody was in: an app left open in the foreground with the screen on
3147
+ * re-snapshots every minute, so it filed a new session every half hour
3148
+ * for as long as it stayed open. Now nothing is captured, uploaded or
3149
+ * handed to the host as a session id until the user is back - a touch,
3150
+ * the soft keyboard, the app returning to the foreground, or an explicit
3151
+ * captureSession() - and that starts the next session (see
3152
+ * maybeRotateSession). In the foreground the session was almost always
3153
+ * paused for idle first (idlePause), so its footage already ended there.
3154
+ */
3155
+ __publicField(this, "sealedForIdle", false);
3123
3156
  __publicField(this, "rotatingSession", false);
3124
3157
  __publicField(this, "rotationPromise", null);
3125
3158
  __publicField(this, "appStateQueue", Promise.resolve());
@@ -3136,9 +3169,58 @@ var MobileReplayRecorder = class {
3136
3169
  __publicField(this, "suppressedTouchPointers", /* @__PURE__ */ new Set());
3137
3170
  __publicField(this, "unidentifiedTouchCount", 0);
3138
3171
  __publicField(this, "unidentifiedTouchPrivate", false);
3172
+ /*
3173
+ * When the person last did something: a touch (any touch - one inside a
3174
+ * ReplayMask is still someone using the app, even though it is never
3175
+ * recorded), the app coming back to the foreground, or the app starting.
3176
+ * Not sessionLastActivityUnixMs, which every recorded event moves: a
3177
+ * route change, a track() call or a JS error the app raises by itself is
3178
+ * the app, not the person, and a polling screen would otherwise never
3179
+ * look idle.
3180
+ */
3181
+ __publicField(this, "lastInputUnixMs", 0);
3182
+ /*
3183
+ * The idle pause (SESSION_REPLAY_IDLE_PAUSE_MS, #4208). Set when the
3184
+ * capture tick finds nobody has touched the app for that long: the open
3185
+ * chunk closes with an idle-paused marker as its last event, the capture
3186
+ * timer stops, and from then on nothing is recorded - no view-tree
3187
+ * sample, no checkout, no chunk, and every event the app emits is
3188
+ * dropped rather than queued. The 15-second flush timer is the only one
3189
+ * left, and all it does is end the session (sealForIdle, #4207) if nobody
3190
+ * comes back inside SESSION_REPLAY_IDLE_ROLLOVER_MS.
3191
+ *
3192
+ * It exists because the recorder used to go on sampling the view tree
3193
+ * every 500 ms, with a full checkout every minute, for the whole half
3194
+ * hour before the idle rollover - the phone's CPU and battery and the
3195
+ * customer's upload and storage, all spent on footage of nobody.
3196
+ *
3197
+ * Input inside that window - a touch, the soft keyboard, the app
3198
+ * returning to the foreground - resumes the same session and tab on a
3199
+ * fresh full snapshot with an idle-resumed marker directly behind it
3200
+ * (idleResumeBacklog), at the next chunk index.
3201
+ */
3202
+ __publicField(this, "idlePause", null);
3203
+ /*
3204
+ * Non-null from the input that ends an idle pause until the snapshot it
3205
+ * resumes on is in the buffer. What is recorded meanwhile - the touch
3206
+ * that woke the recorder, the screen it opened, a track() it caused - is
3207
+ * held here and appended behind that snapshot and the idle-resumed
3208
+ * marker, so the chunk after a pause always opens on its snapshot.
3209
+ */
3210
+ __publicField(this, "idleResumeBacklog", null);
3211
+ /*
3212
+ * The soft keyboard is up. Keystrokes reach no view the recorder can see,
3213
+ * so someone writing a long message - a chat, a form, a review - would
3214
+ * otherwise look exactly like nobody touching the app. While it is up the
3215
+ * idle pause waits, and its showing and hiding are input
3216
+ * (onKeyboardChange). The 30-minute rollover does not look at it.
3217
+ */
3218
+ __publicField(this, "softKeyboardVisible", false);
3219
+ __publicField(this, "keyboardSubscriptions", []);
3139
3220
  this.storage = dependencies.storage ?? defaultReplayStorage;
3140
3221
  this.nativeViewTree = dependencies.nativeViewTree ?? defaultNativeViewTreeAdapter;
3141
3222
  this.appState = dependencies.appState ?? import_react_native2.AppState;
3223
+ this.keyboard = dependencies.keyboard ?? import_react_native2.Keyboard ?? null;
3142
3224
  this.now = dependencies.now ?? defaultNow;
3143
3225
  }
3144
3226
  start(options) {
@@ -3207,6 +3289,7 @@ var MobileReplayRecorder = class {
3207
3289
  this.appStateQueue = Promise.resolve();
3208
3290
  this.rotationPromise = null;
3209
3291
  this.rotatingSession = false;
3292
+ this.sealedForIdle = false;
3210
3293
  this.policyRefreshPromise = null;
3211
3294
  this.lastPolicyRefreshAtUnixMs = 0;
3212
3295
  this.forceFullSnapshot = true;
@@ -3214,6 +3297,8 @@ var MobileReplayRecorder = class {
3214
3297
  this.touchPrivacyMap = CONSERVATIVE_TOUCH_PRIVACY_MAP;
3215
3298
  this.touchQueue = Promise.resolve();
3216
3299
  this.resetTouchSequences();
3300
+ this.idlePause = null;
3301
+ this.idleResumeBacklog = null;
3217
3302
  const validated = validateStartOptions(options);
3218
3303
  if (!validated) {
3219
3304
  this.disable("invalid-start-options");
@@ -3293,6 +3378,7 @@ var MobileReplayRecorder = class {
3293
3378
  );
3294
3379
  }
3295
3380
  this.sessionLastActivityUnixMs = nowUnixMs;
3381
+ this.lastInputUnixMs = nowUnixMs;
3296
3382
  if (!awaitingConsent && config.captureTrigger === "Always" /* Always */ && !this.sampled && !config.isTargeted) {
3297
3383
  this.disable("not-sampled");
3298
3384
  return false;
@@ -3379,12 +3465,23 @@ var MobileReplayRecorder = class {
3379
3465
  this.diagnosticsStatus = "stopped";
3380
3466
  return;
3381
3467
  }
3382
- this.appendCustom(
3383
- VISIBILITY_CUSTOM_EVENT_TAG,
3384
- { state: "stopped" },
3385
- void 0,
3386
- true
3387
- );
3468
+ const endedIdle = this.sealedForIdle || this.rotationReason(this.now()) === "idle";
3469
+ const pause = this.idlePause;
3470
+ if (!endedIdle && pause === null) {
3471
+ this.appendCustom(
3472
+ VISIBILITY_CUSTOM_EVENT_TAG,
3473
+ { state: "stopped" },
3474
+ void 0,
3475
+ true
3476
+ );
3477
+ } else if (!endedIdle && pause !== null && this.isPauseInStream(pause)) {
3478
+ this.idleResumeBacklog = null;
3479
+ this.appendMarker(
3480
+ VISIBILITY_CUSTOM_EVENT_TAG,
3481
+ { state: "stopped" },
3482
+ pause.pausedAtUnixMs
3483
+ );
3484
+ }
3388
3485
  this.haltCapture();
3389
3486
  await this.closeCurrent(true);
3390
3487
  if (this.uploadActive) {
@@ -3532,7 +3629,7 @@ var MobileReplayRecorder = class {
3532
3629
  }
3533
3630
  async setRoute(route) {
3534
3631
  await this.identityQueue;
3535
- if (this.running && !this.acceptingEvents) {
3632
+ if (this.running && (!this.acceptingEvents || this.isIdlePaused())) {
3536
3633
  this.route = sanitizeRoute(route);
3537
3634
  return;
3538
3635
  }
@@ -3547,6 +3644,9 @@ var MobileReplayRecorder = class {
3547
3644
  return;
3548
3645
  }
3549
3646
  this.route = next;
3647
+ if (this.sealedForIdle) {
3648
+ return;
3649
+ }
3550
3650
  this.appendCustom(
3551
3651
  ROUTE_CUSTOM_EVENT_TAG,
3552
3652
  {
@@ -3565,7 +3665,15 @@ var MobileReplayRecorder = class {
3565
3665
  if (!this.running || !this.acceptingEvents) {
3566
3666
  return;
3567
3667
  }
3568
- await this.maybeRotateSession(this.now());
3668
+ const resume = this.foreground ? this.endIdlePause(this.now()) : "none";
3669
+ await this.maybeRotateForUser(this.now());
3670
+ if (resume === "resumed") {
3671
+ this.startTimers();
3672
+ await this.captureTick(true, false);
3673
+ if (!this.running || !this.acceptingEvents) {
3674
+ return;
3675
+ }
3676
+ }
3569
3677
  const safeReason2 = truncate(
3570
3678
  (typeof reason === "string" ? reason.trim() : "") || "manual",
3571
3679
  MAX_CAPTURE_REASON_LENGTH
@@ -3581,7 +3689,13 @@ var MobileReplayRecorder = class {
3581
3689
  if (!this.running || !this.acceptingEvents) {
3582
3690
  return;
3583
3691
  }
3692
+ if (this.isIdlePaused()) {
3693
+ return;
3694
+ }
3584
3695
  await this.maybeRotateSession(this.now());
3696
+ if (this.sealedForIdle) {
3697
+ return;
3698
+ }
3585
3699
  const rawName = error instanceof Error && typeof error.name === "string" ? error.name.trim() : "";
3586
3700
  const name = SAFE_ERROR_NAMES.has(rawName) ? rawName : "Error";
3587
3701
  this.appendCustom(
@@ -3603,9 +3717,18 @@ var MobileReplayRecorder = class {
3603
3717
  if (!this.running || !this.acceptingEvents || !this.foreground) {
3604
3718
  return;
3605
3719
  }
3720
+ const input = this.noteUserInput(this.now());
3721
+ let resumeCapture = null;
3722
+ if (input === "resumed") {
3723
+ this.startTimers();
3724
+ resumeCapture = this.captureTick(true);
3725
+ }
3606
3726
  const generation = this.lifecycleGeneration;
3607
3727
  const operation = this.touchQueue.then(
3608
3728
  async () => {
3729
+ if (resumeCapture) {
3730
+ await resumeCapture;
3731
+ }
3609
3732
  await this.recordTouchAfterPrivacyCheck(sanitizedTouch, generation);
3610
3733
  }
3611
3734
  );
@@ -3632,9 +3755,9 @@ var MobileReplayRecorder = class {
3632
3755
  this.lastTouchMoveAtUnixMs = touch.timestamp;
3633
3756
  }
3634
3757
  const nowUnixMs = this.now();
3635
- if (this.rotatingSession || this.rotationReason(nowUnixMs)) {
3758
+ if (this.rotatingSession || this.sealedForIdle || this.rotationReason(nowUnixMs)) {
3636
3759
  this.diagnostic("touch-dropped-session-rotation");
3637
- void this.maybeRotateSession(nowUnixMs);
3760
+ void this.maybeRotateForUser(nowUnixMs);
3638
3761
  return;
3639
3762
  }
3640
3763
  this.appendEvent(
@@ -3659,8 +3782,9 @@ var MobileReplayRecorder = class {
3659
3782
  }
3660
3783
  }
3661
3784
  getDiagnostics() {
3785
+ const status = this.diagnosticsStatus === "recording" && this.running && this.foreground && (this.isIdlePaused() || this.sealedForIdle) ? "paused" : this.diagnosticsStatus;
3662
3786
  return {
3663
- status: this.diagnosticsStatus,
3787
+ status,
3664
3788
  sessionId: this.identity?.sessionId ?? null,
3665
3789
  recorderKind: MOBILE_RECORDER_KIND,
3666
3790
  configEpoch: this.config?.configEpoch ?? 0,
@@ -3682,9 +3806,14 @@ var MobileReplayRecorder = class {
3682
3806
  )
3683
3807
  };
3684
3808
  }
3685
- /** Current consented replay session for host trace/log correlation. */
3809
+ /**
3810
+ * Current consented replay session for host trace/log correlation. None
3811
+ * while the session has ended idle and nobody is back: a background poll
3812
+ * on an idle app is no part of a recording that ended at the user's last
3813
+ * activity.
3814
+ */
3686
3815
  getSessionId() {
3687
- return this.running && this.identityPersisted ? this.identity?.sessionId ?? null : null;
3816
+ return this.running && this.identityPersisted && !this.sealedForIdle ? this.identity?.sessionId ?? null : null;
3688
3817
  }
3689
3818
  /** Fires immediately when a session exists and again on rotate/clear. */
3690
3819
  onSessionChange(listener) {
@@ -3737,9 +3866,16 @@ var MobileReplayRecorder = class {
3737
3866
  }
3738
3867
  await this.transport?.restore();
3739
3868
  }
3740
- async captureTick(force = false) {
3869
+ /*
3870
+ * mayPauseForIdle is false only for the snapshot captureSession() takes
3871
+ * while paused, whose marker must be in before the pause check runs.
3872
+ */
3873
+ async captureTick(force = false, mayPauseForIdle = true) {
3741
3874
  const generation = this.lifecycleGeneration;
3742
- if (!this.running || !this.acceptingEvents || !this.foreground || !this.rootTag || this.captureGeneration === generation || !this.options) {
3875
+ if (!this.running || !this.acceptingEvents || !this.foreground || !this.rootTag || this.sealedForIdle || this.captureGeneration === generation || !this.options) {
3876
+ return;
3877
+ }
3878
+ if (this.isIdlePaused()) {
3743
3879
  return;
3744
3880
  }
3745
3881
  this.captureGeneration = generation;
@@ -3754,7 +3890,10 @@ var MobileReplayRecorder = class {
3754
3890
  return;
3755
3891
  }
3756
3892
  await this.maybeRotateSession(nowUnixMs);
3757
- if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation)) {
3893
+ if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.sealedForIdle) {
3894
+ return;
3895
+ }
3896
+ if (this.isIdlePaused() || this.idlePause !== null && this.rotationReason(nowUnixMs) !== null) {
3758
3897
  return;
3759
3898
  }
3760
3899
  const capturedSessionId = this.identity?.sessionId;
@@ -3763,7 +3902,6 @@ var MobileReplayRecorder = class {
3763
3902
  return;
3764
3903
  }
3765
3904
  this.touchPrivacyMap = deriveTouchPrivacyMap(tree);
3766
- const size = getViewport();
3767
3905
  const shouldForce = force || this.forceFullSnapshot || nowUnixMs - this.lastFullSnapshotAtUnixMs >= SESSION_REPLAY_CHECKOUT_INTERVAL_MS;
3768
3906
  if (shouldForce && !this.chunkBuffer.isEmpty()) {
3769
3907
  await this.closeCurrent(false);
@@ -3771,25 +3909,13 @@ var MobileReplayRecorder = class {
3771
3909
  if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.identity?.sessionId !== capturedSessionId) {
3772
3910
  return;
3773
3911
  }
3774
- const capture = this.serializer.capture(tree, {
3775
- timestamp: nowUnixMs,
3776
- url: toAppUrl(this.options.mobileAppIdentifier, this.route),
3777
- viewportWidth: size.width,
3778
- viewportHeight: size.height,
3779
- forceFullSnapshot: shouldForce
3780
- });
3781
- this.droppedEvents += capture.droppedNodes;
3782
- this.chunkBuffer.appendMany(capture.events, {
3783
- route: toAppUrl(this.options.mobileAppIdentifier, this.route),
3784
- fidelityNotices: capture.fidelityNotices
3785
- });
3786
- if (capture.hasFullSnapshot) {
3787
- this.lastFullSnapshotAtUnixMs = nowUnixMs;
3788
- this.forceFullSnapshot = false;
3789
- }
3912
+ this.appendCapture(tree, this.options, nowUnixMs, shouldForce);
3790
3913
  if (this.chunkBuffer.shouldFlush()) {
3791
3914
  await this.closeCurrent(false);
3792
3915
  }
3916
+ if (mayPauseForIdle && !this.softKeyboardVisible && nowUnixMs - this.lastInputUnixMs >= SESSION_REPLAY_IDLE_PAUSE_MS) {
3917
+ await this.pauseForIdle(nowUnixMs, generation);
3918
+ }
3793
3919
  } catch (error) {
3794
3920
  if (this.running && this.isCurrentGeneration(generation)) {
3795
3921
  this.disable("native-capture-failed", {
@@ -3803,6 +3929,183 @@ var MobileReplayRecorder = class {
3803
3929
  }
3804
3930
  }
3805
3931
  }
3932
+ appendCapture(tree, options, nowUnixMs, forceFullSnapshot) {
3933
+ const size = getViewport();
3934
+ const url = toAppUrl(options.mobileAppIdentifier, this.route);
3935
+ const capture = this.serializer.capture(tree, {
3936
+ timestamp: nowUnixMs,
3937
+ url,
3938
+ viewportWidth: size.width,
3939
+ viewportHeight: size.height,
3940
+ forceFullSnapshot
3941
+ });
3942
+ this.droppedEvents += capture.droppedNodes;
3943
+ this.chunkBuffer.appendMany(capture.events, {
3944
+ route: url,
3945
+ fidelityNotices: capture.fidelityNotices
3946
+ });
3947
+ if (capture.hasFullSnapshot) {
3948
+ this.lastFullSnapshotAtUnixMs = nowUnixMs;
3949
+ this.forceFullSnapshot = false;
3950
+ this.completeIdleResume(nowUnixMs);
3951
+ }
3952
+ }
3953
+ /*
3954
+ * The person did something: a touch, the soft keyboard showing or
3955
+ * hiding, or the app coming back to the foreground. It ends an idle
3956
+ * pause (see endIdlePause).
3957
+ */
3958
+ noteUserInput(nowUnixMs) {
3959
+ this.lastInputUnixMs = Math.max(this.lastInputUnixMs, nowUnixMs);
3960
+ return this.endIdlePause(nowUnixMs);
3961
+ }
3962
+ /*
3963
+ * What someone being back does to an idle pause, on its own for
3964
+ * captureSession(), which acts on the pause without being input.
3965
+ *
3966
+ * Inside the session's idle window the same session resumes, on the
3967
+ * snapshot of the next capture; the caller starts that capture. Once
3968
+ * the window has run out - or the session already ended idle - the
3969
+ * session is over, and the caller starts the next one through
3970
+ * maybeRotateForUser, as on any idle session; nothing changes here.
3971
+ */
3972
+ endIdlePause(nowUnixMs) {
3973
+ if (this.sealedForIdle) {
3974
+ return "session-ended";
3975
+ }
3976
+ if (!this.isIdlePaused()) {
3977
+ return "none";
3978
+ }
3979
+ if (this.rotationReason(nowUnixMs) !== null) {
3980
+ return "session-ended";
3981
+ }
3982
+ this.idleResumeBacklog = [];
3983
+ this.forceFullSnapshot = true;
3984
+ this.sessionLastActivityUnixMs = Math.max(
3985
+ this.sessionLastActivityUnixMs,
3986
+ nowUnixMs
3987
+ );
3988
+ return "resumed";
3989
+ }
3990
+ /* Paused for idle with nobody back yet: nothing is captured or recorded. */
3991
+ isIdlePaused() {
3992
+ return this.idlePause !== null && this.idleResumeBacklog === null;
3993
+ }
3994
+ /*
3995
+ * Whether the pause's marker is in the stream being recorded now, which
3996
+ * is what owes it the idle-resumed marker and dates a stop at it.
3997
+ */
3998
+ isPauseInStream(pause) {
3999
+ return pause.marked && this.identity?.sessionId === pause.sessionId && this.identity?.tabId === pause.tabId;
4000
+ }
4001
+ /*
4002
+ * Stop capturing: nobody has touched the app for the pause window.
4003
+ *
4004
+ * In this order: the idle-paused marker goes in as the last event of the
4005
+ * open chunk, that chunk goes out through the ordinary path, and the
4006
+ * sampler stops. So the last thing the recording holds before the
4007
+ * stretch nobody recorded says so - and a session the person never comes
4008
+ * back to has already ended there when its idle window runs out, so the
4009
+ * idle seal (sealForIdle) has nothing left to send.
4010
+ *
4011
+ * A recorder holding its pre-roll in memory (error-triggered, or waiting
4012
+ * for consent) has nothing to mark and nothing to send. Its pre-roll is
4013
+ * dropped instead: a trigger once the person is back must not upload a
4014
+ * minute of nobody in front of a snapshot taken after an unrecorded gap.
4015
+ */
4016
+ async pauseForIdle(nowUnixMs, generation) {
4017
+ if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.idlePause !== null || this.sealedForIdle || !this.identity) {
4018
+ return;
4019
+ }
4020
+ const paused = {
4021
+ /*
4022
+ * Never before the session began: one that started with nobody there
4023
+ * (the chunk cap rolled it over) was not idle before it existed.
4024
+ */
4025
+ idleSinceUnixMs: Math.min(
4026
+ nowUnixMs,
4027
+ Math.max(this.lastInputUnixMs, this.identity.sessionStartUnixMs)
4028
+ ),
4029
+ pausedAtUnixMs: nowUnixMs
4030
+ };
4031
+ const marked = this.uploadActive;
4032
+ this.idlePause = {
4033
+ sessionId: this.identity.sessionId,
4034
+ tabId: this.identity.tabId,
4035
+ idleSinceUnixMs: paused.idleSinceUnixMs,
4036
+ pausedAtUnixMs: paused.pausedAtUnixMs,
4037
+ marked
4038
+ };
4039
+ this.stopCaptureTimer();
4040
+ this.diagnostic("idle-paused", {
4041
+ idleForMs: nowUnixMs - paused.idleSinceUnixMs
4042
+ });
4043
+ if (!marked) {
4044
+ this.chunkBuffer.clear();
4045
+ this.rollingBuffer.clear();
4046
+ return;
4047
+ }
4048
+ this.appendMarker(IDLE_PAUSED_CUSTOM_EVENT_TAG, paused, nowUnixMs);
4049
+ await this.closeCurrent(false);
4050
+ }
4051
+ /*
4052
+ * The snapshot the session resumes on is in the buffer: the idle-resumed
4053
+ * marker goes directly behind it, then whatever was recorded since the
4054
+ * input that woke the recorder. Same session, same tab and - the pause
4055
+ * having closed its chunk - the next chunk index.
4056
+ */
4057
+ completeIdleResume(nowUnixMs) {
4058
+ const pause = this.idlePause;
4059
+ const held = this.idleResumeBacklog;
4060
+ if (pause === null || held === null) {
4061
+ return;
4062
+ }
4063
+ this.idlePause = null;
4064
+ this.idleResumeBacklog = null;
4065
+ const resumedAtUnixMs = Math.max(pause.pausedAtUnixMs, nowUnixMs);
4066
+ if (this.isPauseInStream(pause)) {
4067
+ const resumed = {
4068
+ pausedAtUnixMs: pause.pausedAtUnixMs,
4069
+ resumedAtUnixMs
4070
+ };
4071
+ this.appendMarker(
4072
+ IDLE_RESUMED_CUSTOM_EVENT_TAG,
4073
+ resumed,
4074
+ resumedAtUnixMs
4075
+ );
4076
+ }
4077
+ this.releaseHeldEvents(held);
4078
+ this.diagnostic("idle-resumed", {
4079
+ pausedForMs: resumedAtUnixMs - pause.pausedAtUnixMs
4080
+ });
4081
+ }
4082
+ /*
4083
+ * A marker the recorder writes about itself - the idle pause and resume,
4084
+ * and a stop while paused - straight into the open chunk, stamped with
4085
+ * the moment it stands for. None of them is activity (the idle rollover
4086
+ * must not move for them), and none is "now": a stop while paused is
4087
+ * dated at the pause.
4088
+ */
4089
+ appendMarker(tag, payload, timestamp) {
4090
+ const options = {};
4091
+ if (this.options) {
4092
+ options.route = toAppUrl(this.options.mobileAppIdentifier, this.route);
4093
+ }
4094
+ if (!this.chunkBuffer.append(
4095
+ createCustomEvent(tag, payload, timestamp),
4096
+ options
4097
+ )) {
4098
+ this.droppedEvents += 1;
4099
+ }
4100
+ }
4101
+ /* What was held for a resume, in the order it was recorded. */
4102
+ releaseHeldEvents(held) {
4103
+ for (const entry of held) {
4104
+ if (!this.chunkBuffer.append(entry.event, entry.options)) {
4105
+ this.droppedEvents += 1;
4106
+ }
4107
+ }
4108
+ }
3806
4109
  appendCustom(tag, payload, signal, allowWhenQuiesced = false) {
3807
4110
  this.appendEvent(
3808
4111
  createCustomEvent(tag, payload, this.now()),
@@ -3814,15 +4117,17 @@ var MobileReplayRecorder = class {
3814
4117
  if (!this.running || !this.acceptingEvents && !allowWhenQuiesced) {
3815
4118
  return;
3816
4119
  }
4120
+ if (this.sealedForIdle) {
4121
+ return;
4122
+ }
3817
4123
  if (!Number.isFinite(event.timestamp)) {
3818
4124
  this.droppedEvents += 1;
3819
4125
  this.diagnostic("invalid-event-timestamp");
3820
4126
  return;
3821
4127
  }
3822
- this.sessionLastActivityUnixMs = Math.max(
3823
- this.sessionLastActivityUnixMs,
3824
- event.timestamp
3825
- );
4128
+ if (this.isIdlePaused()) {
4129
+ return;
4130
+ }
3826
4131
  const appendOptions = {};
3827
4132
  if (this.options) {
3828
4133
  appendOptions.route = toAppUrl(
@@ -3833,6 +4138,18 @@ var MobileReplayRecorder = class {
3833
4138
  if (signal) {
3834
4139
  appendOptions.signal = signal;
3835
4140
  }
4141
+ if (this.idleResumeBacklog !== null) {
4142
+ if (this.idleResumeBacklog.length < MAX_EVENTS_HELD_FOR_RESUME) {
4143
+ this.idleResumeBacklog.push({ event, options: appendOptions });
4144
+ } else {
4145
+ this.droppedEvents += 1;
4146
+ }
4147
+ return;
4148
+ }
4149
+ this.sessionLastActivityUnixMs = Math.max(
4150
+ this.sessionLastActivityUnixMs,
4151
+ event.timestamp
4152
+ );
3836
4153
  const accepted = this.chunkBuffer.append(event, appendOptions);
3837
4154
  if (!accepted) {
3838
4155
  this.droppedEvents += 1;
@@ -3842,7 +4159,7 @@ var MobileReplayRecorder = class {
3842
4159
  }
3843
4160
  }
3844
4161
  async closeCurrent(isFinal) {
3845
- const closesFinalChunkSlot = !isFinal && !this.rotatingSession && this.uploadActive && this.sessionStore?.getNextChunkIndex() === MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1;
4162
+ const closesFinalChunkSlot = !isFinal && !this.rotatingSession && !this.sealedForIdle && this.uploadActive && this.sessionStore?.getNextChunkIndex() === MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1;
3846
4163
  if (closesFinalChunkSlot) {
3847
4164
  this.rotatingSession = true;
3848
4165
  const rotationOptions = {};
@@ -3854,7 +4171,7 @@ var MobileReplayRecorder = class {
3854
4171
  }
3855
4172
  this.chunkBuffer.append(
3856
4173
  createCustomEvent(
3857
- "oneuptime.session-rotated",
4174
+ SESSION_ROTATED_CUSTOM_EVENT_TAG,
3858
4175
  { reason: "chunk-cap" },
3859
4176
  this.now()
3860
4177
  ),
@@ -4018,6 +4335,63 @@ var MobileReplayRecorder = class {
4018
4335
  });
4019
4336
  }
4020
4337
  );
4338
+ this.installKeyboardListeners();
4339
+ }
4340
+ installKeyboardListeners() {
4341
+ this.removeKeyboardListeners();
4342
+ const keyboard = this.keyboard;
4343
+ this.softKeyboardVisible = false;
4344
+ if (!keyboard || typeof keyboard.addListener !== "function") {
4345
+ return;
4346
+ }
4347
+ try {
4348
+ this.softKeyboardVisible = keyboard.isVisible?.() === true;
4349
+ } catch {
4350
+ this.softKeyboardVisible = false;
4351
+ }
4352
+ try {
4353
+ this.keyboardSubscriptions.push(
4354
+ keyboard.addListener("keyboardDidShow", () => {
4355
+ this.onKeyboardChange(true);
4356
+ }),
4357
+ keyboard.addListener("keyboardDidHide", () => {
4358
+ this.onKeyboardChange(false);
4359
+ })
4360
+ );
4361
+ } catch {
4362
+ this.removeKeyboardListeners();
4363
+ this.softKeyboardVisible = false;
4364
+ this.diagnostic("keyboard-subscribe-failed");
4365
+ }
4366
+ }
4367
+ removeKeyboardListeners() {
4368
+ const subscriptions = this.keyboardSubscriptions;
4369
+ this.keyboardSubscriptions = [];
4370
+ for (const subscription of subscriptions) {
4371
+ try {
4372
+ subscription.remove();
4373
+ } catch {
4374
+ }
4375
+ }
4376
+ }
4377
+ /*
4378
+ * The soft keyboard came up or went away. That is the person - a field
4379
+ * tapped, a message sent - so it counts as input and resumes a paused
4380
+ * recording the way a touch does; and while it is up the idle pause
4381
+ * waits, since what is typed on it reaches no view the recorder sees.
4382
+ */
4383
+ onKeyboardChange(visible) {
4384
+ this.softKeyboardVisible = visible;
4385
+ if (!this.running || !this.acceptingEvents || !this.foreground) {
4386
+ return;
4387
+ }
4388
+ const input = this.noteUserInput(this.now());
4389
+ if (input === "resumed") {
4390
+ this.startTimers();
4391
+ void this.captureTick(true);
4392
+ } else if (input === "session-ended") {
4393
+ void this.maybeRotateForUser(this.now());
4394
+ }
4021
4395
  }
4022
4396
  async onAppStateChange(state, generation) {
4023
4397
  if (!this.running || !this.isCurrentGeneration(generation)) {
@@ -4037,6 +4411,13 @@ var MobileReplayRecorder = class {
4037
4411
  if (!this.running || !this.isCurrentGeneration(generation)) {
4038
4412
  return;
4039
4413
  }
4414
+ if (this.idlePause !== null || this.sealedForIdle) {
4415
+ this.idleResumeBacklog = null;
4416
+ if (this.uploadActive) {
4417
+ await this.transport?.drain();
4418
+ }
4419
+ return;
4420
+ }
4040
4421
  this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "hidden" });
4041
4422
  await this.closeCurrent(false);
4042
4423
  if (this.uploadActive) {
@@ -4046,6 +4427,7 @@ var MobileReplayRecorder = class {
4046
4427
  }
4047
4428
  if (isActive && !this.foreground) {
4048
4429
  this.foreground = true;
4430
+ const input = this.noteUserInput(this.now());
4049
4431
  const policyReady = await this.refreshRuntimePolicy(
4050
4432
  generation,
4051
4433
  true,
@@ -4054,15 +4436,22 @@ var MobileReplayRecorder = class {
4054
4436
  if (!policyReady) {
4055
4437
  return;
4056
4438
  }
4057
- await this.maybeRotateSession(this.now());
4439
+ const sessionIdBeforeResume = this.identity?.sessionId;
4440
+ await this.maybeRotateSession(this.now(), true);
4058
4441
  if (!this.running || !this.isCurrentGeneration(generation)) {
4059
4442
  return;
4060
4443
  }
4061
- this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "visible" });
4062
- this.forceFullSnapshot = true;
4444
+ const resumedPausedSession = input === "resumed" && this.identity?.sessionId === sessionIdBeforeResume;
4445
+ if (!resumedPausedSession) {
4446
+ this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "visible" });
4447
+ }
4448
+ const openedOnSnapshot = this.identity?.sessionId !== sessionIdBeforeResume && this.lastFullSnapshotAtUnixMs > 0;
4449
+ if (!openedOnSnapshot) {
4450
+ this.forceFullSnapshot = true;
4451
+ }
4063
4452
  this.startTimers();
4064
4453
  this.diagnosticsStatus = this.consentState() === "Unknown" ? "consent-required" : "recording";
4065
- await this.captureTick(true);
4454
+ await this.captureTick(!openedOnSnapshot);
4066
4455
  if (this.uploadActive && this.running) {
4067
4456
  this.transport?.resume();
4068
4457
  }
@@ -4081,16 +4470,31 @@ var MobileReplayRecorder = class {
4081
4470
  void this.captureTick();
4082
4471
  }, intervalMs);
4083
4472
  this.flushTimer = setInterval(() => {
4473
+ if (this.isIdlePaused()) {
4474
+ if (this.acceptingEvents) {
4475
+ void this.maybeRotateSession(this.now());
4476
+ }
4477
+ return;
4478
+ }
4084
4479
  if (this.acceptingEvents) {
4085
4480
  void this.closeCurrent(false);
4086
4481
  }
4087
4482
  }, SESSION_REPLAY_FLUSH_INTERVAL_MS);
4088
4483
  }
4089
- clearTimers() {
4484
+ /*
4485
+ * The sampler alone. An idle pause stops it - every tick is a native
4486
+ * traversal of the whole view tree, which is what a pause is for not
4487
+ * paying - and keeps the 15-second flush timer as its heartbeat, rather
4488
+ * than waking the JS thread twice a second to find nothing to do.
4489
+ */
4490
+ stopCaptureTimer() {
4090
4491
  if (this.captureTimer) {
4091
4492
  clearInterval(this.captureTimer);
4092
4493
  this.captureTimer = null;
4093
4494
  }
4495
+ }
4496
+ clearTimers() {
4497
+ this.stopCaptureTimer();
4094
4498
  if (this.flushTimer) {
4095
4499
  clearInterval(this.flushTimer);
4096
4500
  this.flushTimer = null;
@@ -4182,6 +4586,7 @@ var MobileReplayRecorder = class {
4182
4586
  this.clearTimers();
4183
4587
  this.appStateSubscription?.remove();
4184
4588
  this.appStateSubscription = null;
4589
+ this.removeKeyboardListeners();
4185
4590
  this.stopConnectivity();
4186
4591
  this.restoreGlobalErrorHandler();
4187
4592
  }
@@ -4342,9 +4747,16 @@ var MobileReplayRecorder = class {
4342
4747
  if (!this.running || identityEpoch !== this.identityCancellationEpoch) {
4343
4748
  return;
4344
4749
  }
4750
+ if (!this.rotationPromise && !this.sealedForIdle) {
4751
+ if (this.isIdlePaused()) {
4752
+ await this.sealForIdleNow();
4753
+ } else if (this.rotationReason(this.now()) === "idle") {
4754
+ await this.maybeRotateSession(this.now());
4755
+ }
4756
+ }
4345
4757
  if (this.rotationPromise) {
4346
4758
  await this.rotationPromise;
4347
- } else {
4759
+ } else if (!this.sealedForIdle) {
4348
4760
  this.rotatingSession = true;
4349
4761
  const rotation = this.performSessionRotation(
4350
4762
  this.now(),
@@ -4679,16 +5091,24 @@ var MobileReplayRecorder = class {
4679
5091
  chunkIndex: 0
4680
5092
  };
4681
5093
  }
5094
+ /*
5095
+ * Idle before the duration cap, the order Common's SessionIdentity decides
5096
+ * them in. A session left for the idle window ended where it was left,
5097
+ * even when it has outlived the cap by the time anything notices, and
5098
+ * only an idle rotation dates its end there (see performSessionRotation):
5099
+ * called "duration", a session three hours old put in the background for
5100
+ * two more was sealed two hours after its footage.
5101
+ */
4682
5102
  rotationReason(nowUnixMs) {
4683
5103
  if (!this.identity) {
4684
5104
  return null;
4685
5105
  }
4686
- if (nowUnixMs - this.identity.sessionStartUnixMs >= SESSION_REPLAY_MAX_SESSION_MS) {
4687
- return "duration";
4688
- }
4689
5106
  if (nowUnixMs - this.sessionLastActivityUnixMs >= SESSION_REPLAY_IDLE_ROLLOVER_MS) {
4690
5107
  return "idle";
4691
5108
  }
5109
+ if (nowUnixMs - this.identity.sessionStartUnixMs >= SESSION_REPLAY_MAX_SESSION_MS) {
5110
+ return "duration";
5111
+ }
4692
5112
  return null;
4693
5113
  }
4694
5114
  runExternalActivity(nowUnixMs, action) {
@@ -4723,23 +5143,61 @@ var MobileReplayRecorder = class {
4723
5143
  }
4724
5144
  });
4725
5145
  }
4726
- async maybeRotateSession(nowUnixMs) {
4727
- if (this.rotationPromise) {
5146
+ /*
5147
+ * isUserBack: the caller knows the user is here - a touch, the app
5148
+ * returning to the foreground, an explicit captureSession(). An idle
5149
+ * session then rotates at once. Anything else (a capture tick, a route
5150
+ * change, a developer event) only finds out the session went idle, and
5151
+ * seals it to wait for them (see sealedForIdle).
5152
+ */
5153
+ async maybeRotateSession(nowUnixMs, isUserBack = false) {
5154
+ while (this.rotationPromise) {
4728
5155
  await this.rotationPromise;
4729
- return;
5156
+ if (!isUserBack || !this.sealedForIdle) {
5157
+ return;
5158
+ }
4730
5159
  }
4731
5160
  if (!this.running || this.rotatingSession || !this.identity || !this.config || !this.sessionStore || !this.options) {
4732
5161
  return;
4733
5162
  }
4734
- const reason = this.rotationReason(nowUnixMs);
5163
+ let reason;
5164
+ if (this.sealedForIdle) {
5165
+ if (!isUserBack) {
5166
+ return;
5167
+ }
5168
+ reason = "idle";
5169
+ } else {
5170
+ reason = this.rotationReason(nowUnixMs);
5171
+ if (reason !== null && this.isIdlePaused()) {
5172
+ reason = "idle";
5173
+ }
5174
+ }
4735
5175
  if (!reason) {
4736
5176
  return;
4737
5177
  }
4738
5178
  this.rotatingSession = true;
4739
- const operation = this.performSessionRotation(
4740
- nowUnixMs,
4741
- reason
4742
- );
5179
+ const operation = reason === "idle" && !isUserBack ? this.sealForIdle() : this.performSessionRotation(nowUnixMs, reason);
5180
+ this.rotationPromise = operation;
5181
+ try {
5182
+ await operation;
5183
+ } finally {
5184
+ if (this.rotationPromise === operation) {
5185
+ this.rotationPromise = null;
5186
+ }
5187
+ this.rotatingSession = false;
5188
+ }
5189
+ }
5190
+ /*
5191
+ * The idle seal on demand, under the lock every rotation takes: for an
5192
+ * identity change on a session paused for idle, which no clock has ended
5193
+ * yet but nobody is in.
5194
+ */
5195
+ async sealForIdleNow() {
5196
+ if (this.rotationPromise || this.rotatingSession || this.sealedForIdle) {
5197
+ return;
5198
+ }
5199
+ this.rotatingSession = true;
5200
+ const operation = this.sealForIdle();
4743
5201
  this.rotationPromise = operation;
4744
5202
  try {
4745
5203
  await operation;
@@ -4750,8 +5208,68 @@ var MobileReplayRecorder = class {
4750
5208
  this.rotatingSession = false;
4751
5209
  }
4752
5210
  }
5211
+ /*
5212
+ * maybeRotateSession for a user who is here. On an app whose session
5213
+ * ended idle - or was paused for idle, which stops the capture tick too
5214
+ * (#4208) - the policy is brought up to date first: no capture tick has
5215
+ * refreshed it while the app sat idle, and the next session must not open
5216
+ * under one that has since changed (or turned replay off).
5217
+ */
5218
+ async maybeRotateForUser(nowUnixMs) {
5219
+ if ((this.sealedForIdle || this.idlePause !== null) && !this.rotatingSession) {
5220
+ const policyReady = await this.refreshRuntimePolicy(
5221
+ this.lifecycleGeneration,
5222
+ false,
5223
+ "periodic"
5224
+ );
5225
+ if (!policyReady) {
5226
+ return;
5227
+ }
5228
+ }
5229
+ await this.maybeRotateSession(nowUnixMs, true);
5230
+ }
5231
+ /*
5232
+ * End the session as idle, where it really ended, and wait for the user.
5233
+ *
5234
+ * Sealed with whatever footage is still buffered - dated by that footage,
5235
+ * never by now - and with no marker of its own: a marker stamped now
5236
+ * would date the session's end half an hour or more after anything the
5237
+ * player can show. With nothing buffered nothing is sent, and the server
5238
+ * ends the session at its last chunk - for a session paused for idle
5239
+ * (#4208), the chunk its idle-paused marker closed.
5240
+ */
5241
+ async sealForIdle() {
5242
+ this.sealedForIdle = true;
5243
+ this.idlePause = null;
5244
+ this.idleResumeBacklog = null;
5245
+ this.clearTimers();
5246
+ this.diagnostic("session-ended-idle", {
5247
+ idleForMs: Math.max(0, this.now() - this.sessionLastActivityUnixMs)
5248
+ });
5249
+ this.notifySessionChange();
5250
+ await this.closeCurrent(true);
5251
+ await this.closeQueue;
5252
+ if (!this.running) {
5253
+ return;
5254
+ }
5255
+ if (this.uploadActive) {
5256
+ await this.transport?.drain();
5257
+ } else {
5258
+ this.rollingBuffer.clear();
5259
+ }
5260
+ if (this.running && this.identityPersisted) {
5261
+ await this.sessionStore?.end();
5262
+ }
5263
+ }
4753
5264
  async performSessionRotation(nowUnixMs, reason) {
4754
- this.appendCustom("oneuptime.session-rotated", { reason }, void 0, true);
5265
+ if (reason !== "idle") {
5266
+ this.appendCustom(
5267
+ SESSION_ROTATED_CUSTOM_EVENT_TAG,
5268
+ { reason },
5269
+ void 0,
5270
+ true
5271
+ );
5272
+ }
4755
5273
  await this.closeCurrent(true);
4756
5274
  await this.closeQueue;
4757
5275
  if (!this.running) {
@@ -4766,12 +5284,66 @@ var MobileReplayRecorder = class {
4766
5284
  return;
4767
5285
  }
4768
5286
  await this.completeSessionIdentityRotation(nowUnixMs, reason);
5287
+ if (reason === "idle" && this.running) {
5288
+ await this.openSessionAfterIdle();
5289
+ }
5290
+ }
5291
+ /*
5292
+ * The first things a session after an idle one records: a full snapshot
5293
+ * of the screen the user came back to, then the rotation marker - the
5294
+ * snapshot first, so chunk 0 is a seek anchor rather than a lone marker.
5295
+ *
5296
+ * Taken here, inside the rotation, rather than left to the next capture
5297
+ * tick: until the rotation settles, touches are dropped and every other
5298
+ * event waits for it (rotatingSession), so nothing can land ahead of the
5299
+ * snapshot. A user who has just come back is touching the screen, and
5300
+ * half a second of that was otherwise a chunk 0 with no screen in it.
5301
+ *
5302
+ * In the background or without a registered root there is no screen to
5303
+ * capture yet; the marker opens the session alone.
5304
+ */
5305
+ async openSessionAfterIdle() {
5306
+ const generation = this.lifecycleGeneration;
5307
+ const rootTag = this.rootTag;
5308
+ const options = this.options;
5309
+ const sessionId = this.identity?.sessionId;
5310
+ if (this.foreground && rootTag && options && sessionId) {
5311
+ const capturedAtUnixMs = this.now();
5312
+ let tree;
5313
+ try {
5314
+ tree = await this.nativeViewTree.captureViewTree(rootTag);
5315
+ } catch (error) {
5316
+ if (this.running && this.isCurrentGeneration(generation)) {
5317
+ this.disable("native-capture-failed", {
5318
+ name: error instanceof Error ? error.name : "Error"
5319
+ });
5320
+ this.haltCapture();
5321
+ }
5322
+ return;
5323
+ }
5324
+ if (!this.running || !this.isCurrentGeneration(generation) || this.identity?.sessionId !== sessionId) {
5325
+ return;
5326
+ }
5327
+ if (this.foreground) {
5328
+ this.touchPrivacyMap = deriveTouchPrivacyMap(tree);
5329
+ this.appendCapture(tree, options, capturedAtUnixMs, true);
5330
+ }
5331
+ }
5332
+ this.appendCustom(
5333
+ SESSION_ROTATED_CUSTOM_EVENT_TAG,
5334
+ { reason: "idle" },
5335
+ void 0,
5336
+ true
5337
+ );
4769
5338
  }
4770
5339
  async completeSessionIdentityRotation(nowUnixMs, reason) {
4771
5340
  if (!this.sessionStore || !this.config || !this.options) {
4772
5341
  return;
4773
5342
  }
4774
5343
  this.identity = this.identityPersisted ? await this.sessionStore.rotate(nowUnixMs) : this.ephemeralIdentity(nowUnixMs);
5344
+ this.sealedForIdle = false;
5345
+ this.idlePause = null;
5346
+ this.idleResumeBacklog = null;
4775
5347
  this.notifySessionChange();
4776
5348
  this.sampled = this.identityPersisted ? deterministicSample(
4777
5349
  this.identity.sessionId,
@@ -4792,6 +5364,10 @@ var MobileReplayRecorder = class {
4792
5364
  if (this.config.captureTrigger === "Always" /* Always */ && this.identityPersisted && !this.sampled && !this.config.isTargeted) {
4793
5365
  this.disable("not-sampled");
4794
5366
  this.haltCapture();
5367
+ return;
5368
+ }
5369
+ if (this.captureTimer === null && this.running && this.foreground) {
5370
+ this.startTimers();
4795
5371
  }
4796
5372
  }
4797
5373
  disable(code, details) {