@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.js CHANGED
@@ -15,7 +15,7 @@ import {
15
15
  } from "react-native";
16
16
 
17
17
  // src/MobileReplayRecorder.ts
18
- import { AppState } from "react-native";
18
+ import { AppState, Keyboard } from "react-native";
19
19
 
20
20
  // src/Contract.ts
21
21
  var SESSION_REPLAY_WIRE_VERSION = 1;
@@ -51,6 +51,7 @@ var SESSION_REPLAY_CHECKOUT_INTERVAL_MS = 6e4;
51
51
  var SESSION_REPLAY_ROLLING_BUFFER_MS = 6e4;
52
52
  var SESSION_REPLAY_ROLLING_BUFFER_BYTES = 2 * 1024 * 1024;
53
53
  var SESSION_REPLAY_IDLE_ROLLOVER_MS = 30 * 6e4;
54
+ var SESSION_REPLAY_IDLE_PAUSE_MS = 5 * 6e4;
54
55
  var SESSION_REPLAY_MAX_SESSION_MS = 4 * 60 * 6e4;
55
56
  var SESSION_REPLAY_MAX_DECOMPRESSED_FRAME_BYTES = 8 * 1024 * 1024;
56
57
  var MAX_SESSION_REPLAY_CHUNK_BYTES = 2 * 1024 * 1024;
@@ -59,7 +60,7 @@ var SESSION_REPLAY_MAX_OFFLINE_DELAY_MS = 3 * 24 * 60 * 60 * 1e3;
59
60
  var MOBILE_CAPTURE_INTERVAL_MS = 500;
60
61
  var MOBILE_RECORDER_KIND = "rn-view-tree";
61
62
  var SYNTHETIC_RRWEB_VERSION = "synthetic-1";
62
- var RECORDER_VERSION = true ? "14.0.11" : "0.0.0-dev";
63
+ var RECORDER_VERSION = true ? "14.0.13" : "0.0.0-dev";
63
64
  var SESSION_REPLAY_MOBILE_RECORDER_CAPABILITIES = [
64
65
  "mobile-view-tree",
65
66
  "mobile-touch-events",
@@ -111,6 +112,9 @@ var IDENTIFY_CUSTOM_EVENT_TAG = "oneuptime.identify";
111
112
  var TAGS_CUSTOM_EVENT_TAG = "oneuptime.tags";
112
113
  var VISIBILITY_CUSTOM_EVENT_TAG = "oneuptime.visibility";
113
114
  var TOUCH_CUSTOM_EVENT_TAG = "oneuptime.touch";
115
+ var SESSION_ROTATED_CUSTOM_EVENT_TAG = "oneuptime.session-rotated";
116
+ var IDLE_PAUSED_CUSTOM_EVENT_TAG = "oneuptime.idle-paused";
117
+ var IDLE_RESUMED_CUSTOM_EVENT_TAG = "oneuptime.idle-resumed";
114
118
 
115
119
  // src/Sanitize.ts
116
120
  var MAX_TRAIT_KEYS = 20;
@@ -2486,6 +2490,18 @@ var ReplaySessionStore = class {
2486
2490
  };
2487
2491
  return { ...this.identity };
2488
2492
  }
2493
+ /*
2494
+ * Forget the stored session without starting the next one. A session
2495
+ * that ends while the recorder is paused for idle has no successor until
2496
+ * the person comes back, and its record - last written by its own final
2497
+ * chunk, so it looks recent - would otherwise let a relaunch in the
2498
+ * meantime carry on a session that is already over. The visitor id stays,
2499
+ * and so does the in-memory identity: nothing more is sent under it, and
2500
+ * the next rotate() writes the new session.
2501
+ */
2502
+ async end() {
2503
+ await this.safeRemove(this.sessionKey);
2504
+ }
2489
2505
  async clear() {
2490
2506
  this.identity = null;
2491
2507
  await Promise.all([
@@ -2944,6 +2960,7 @@ function isTouchPrivate(privacyMap, x2, y) {
2944
2960
  var MAX_DIAGNOSTIC_EVENTS = 100;
2945
2961
  var TOUCH_MOVE_SAMPLE_MS = 50;
2946
2962
  var MOBILE_POLICY_REFRESH_INTERVAL_MS = 5 * 6e4;
2963
+ var MAX_EVENTS_HELD_FOR_RESUME = 500;
2947
2964
  var SAFE_ERROR_NAMES = /* @__PURE__ */ new Set([
2948
2965
  "AggregateError",
2949
2966
  "Error",
@@ -3015,6 +3032,7 @@ var MobileReplayRecorder = class {
3015
3032
  __publicField(this, "storage");
3016
3033
  __publicField(this, "nativeViewTree");
3017
3034
  __publicField(this, "appState");
3035
+ __publicField(this, "keyboard");
3018
3036
  __publicField(this, "now");
3019
3037
  __publicField(this, "serializer", new ViewTreeSerializer());
3020
3038
  __publicField(this, "chunkBuffer", new ReplayChunkBuffer());
@@ -3081,6 +3099,21 @@ var MobileReplayRecorder = class {
3081
3099
  __publicField(this, "pendingStartCancellation", null);
3082
3100
  __publicField(this, "identityPersisted", false);
3083
3101
  __publicField(this, "sessionLastActivityUnixMs", 0);
3102
+ /*
3103
+ * The session went the idle window without activity, was sealed where it
3104
+ * ended, and nobody has come back since.
3105
+ *
3106
+ * Rotating on the spot instead (the old behaviour) started a session
3107
+ * nobody was in: an app left open in the foreground with the screen on
3108
+ * re-snapshots every minute, so it filed a new session every half hour
3109
+ * for as long as it stayed open. Now nothing is captured, uploaded or
3110
+ * handed to the host as a session id until the user is back - a touch,
3111
+ * the soft keyboard, the app returning to the foreground, or an explicit
3112
+ * captureSession() - and that starts the next session (see
3113
+ * maybeRotateSession). In the foreground the session was almost always
3114
+ * paused for idle first (idlePause), so its footage already ended there.
3115
+ */
3116
+ __publicField(this, "sealedForIdle", false);
3084
3117
  __publicField(this, "rotatingSession", false);
3085
3118
  __publicField(this, "rotationPromise", null);
3086
3119
  __publicField(this, "appStateQueue", Promise.resolve());
@@ -3097,9 +3130,58 @@ var MobileReplayRecorder = class {
3097
3130
  __publicField(this, "suppressedTouchPointers", /* @__PURE__ */ new Set());
3098
3131
  __publicField(this, "unidentifiedTouchCount", 0);
3099
3132
  __publicField(this, "unidentifiedTouchPrivate", false);
3133
+ /*
3134
+ * When the person last did something: a touch (any touch - one inside a
3135
+ * ReplayMask is still someone using the app, even though it is never
3136
+ * recorded), the app coming back to the foreground, or the app starting.
3137
+ * Not sessionLastActivityUnixMs, which every recorded event moves: a
3138
+ * route change, a track() call or a JS error the app raises by itself is
3139
+ * the app, not the person, and a polling screen would otherwise never
3140
+ * look idle.
3141
+ */
3142
+ __publicField(this, "lastInputUnixMs", 0);
3143
+ /*
3144
+ * The idle pause (SESSION_REPLAY_IDLE_PAUSE_MS, #4208). Set when the
3145
+ * capture tick finds nobody has touched the app for that long: the open
3146
+ * chunk closes with an idle-paused marker as its last event, the capture
3147
+ * timer stops, and from then on nothing is recorded - no view-tree
3148
+ * sample, no checkout, no chunk, and every event the app emits is
3149
+ * dropped rather than queued. The 15-second flush timer is the only one
3150
+ * left, and all it does is end the session (sealForIdle, #4207) if nobody
3151
+ * comes back inside SESSION_REPLAY_IDLE_ROLLOVER_MS.
3152
+ *
3153
+ * It exists because the recorder used to go on sampling the view tree
3154
+ * every 500 ms, with a full checkout every minute, for the whole half
3155
+ * hour before the idle rollover - the phone's CPU and battery and the
3156
+ * customer's upload and storage, all spent on footage of nobody.
3157
+ *
3158
+ * Input inside that window - a touch, the soft keyboard, the app
3159
+ * returning to the foreground - resumes the same session and tab on a
3160
+ * fresh full snapshot with an idle-resumed marker directly behind it
3161
+ * (idleResumeBacklog), at the next chunk index.
3162
+ */
3163
+ __publicField(this, "idlePause", null);
3164
+ /*
3165
+ * Non-null from the input that ends an idle pause until the snapshot it
3166
+ * resumes on is in the buffer. What is recorded meanwhile - the touch
3167
+ * that woke the recorder, the screen it opened, a track() it caused - is
3168
+ * held here and appended behind that snapshot and the idle-resumed
3169
+ * marker, so the chunk after a pause always opens on its snapshot.
3170
+ */
3171
+ __publicField(this, "idleResumeBacklog", null);
3172
+ /*
3173
+ * The soft keyboard is up. Keystrokes reach no view the recorder can see,
3174
+ * so someone writing a long message - a chat, a form, a review - would
3175
+ * otherwise look exactly like nobody touching the app. While it is up the
3176
+ * idle pause waits, and its showing and hiding are input
3177
+ * (onKeyboardChange). The 30-minute rollover does not look at it.
3178
+ */
3179
+ __publicField(this, "softKeyboardVisible", false);
3180
+ __publicField(this, "keyboardSubscriptions", []);
3100
3181
  this.storage = dependencies.storage ?? defaultReplayStorage;
3101
3182
  this.nativeViewTree = dependencies.nativeViewTree ?? defaultNativeViewTreeAdapter;
3102
3183
  this.appState = dependencies.appState ?? AppState;
3184
+ this.keyboard = dependencies.keyboard ?? Keyboard ?? null;
3103
3185
  this.now = dependencies.now ?? defaultNow;
3104
3186
  }
3105
3187
  start(options) {
@@ -3168,6 +3250,7 @@ var MobileReplayRecorder = class {
3168
3250
  this.appStateQueue = Promise.resolve();
3169
3251
  this.rotationPromise = null;
3170
3252
  this.rotatingSession = false;
3253
+ this.sealedForIdle = false;
3171
3254
  this.policyRefreshPromise = null;
3172
3255
  this.lastPolicyRefreshAtUnixMs = 0;
3173
3256
  this.forceFullSnapshot = true;
@@ -3175,6 +3258,8 @@ var MobileReplayRecorder = class {
3175
3258
  this.touchPrivacyMap = CONSERVATIVE_TOUCH_PRIVACY_MAP;
3176
3259
  this.touchQueue = Promise.resolve();
3177
3260
  this.resetTouchSequences();
3261
+ this.idlePause = null;
3262
+ this.idleResumeBacklog = null;
3178
3263
  const validated = validateStartOptions(options);
3179
3264
  if (!validated) {
3180
3265
  this.disable("invalid-start-options");
@@ -3254,6 +3339,7 @@ var MobileReplayRecorder = class {
3254
3339
  );
3255
3340
  }
3256
3341
  this.sessionLastActivityUnixMs = nowUnixMs;
3342
+ this.lastInputUnixMs = nowUnixMs;
3257
3343
  if (!awaitingConsent && config.captureTrigger === "Always" /* Always */ && !this.sampled && !config.isTargeted) {
3258
3344
  this.disable("not-sampled");
3259
3345
  return false;
@@ -3340,12 +3426,23 @@ var MobileReplayRecorder = class {
3340
3426
  this.diagnosticsStatus = "stopped";
3341
3427
  return;
3342
3428
  }
3343
- this.appendCustom(
3344
- VISIBILITY_CUSTOM_EVENT_TAG,
3345
- { state: "stopped" },
3346
- void 0,
3347
- true
3348
- );
3429
+ const endedIdle = this.sealedForIdle || this.rotationReason(this.now()) === "idle";
3430
+ const pause = this.idlePause;
3431
+ if (!endedIdle && pause === null) {
3432
+ this.appendCustom(
3433
+ VISIBILITY_CUSTOM_EVENT_TAG,
3434
+ { state: "stopped" },
3435
+ void 0,
3436
+ true
3437
+ );
3438
+ } else if (!endedIdle && pause !== null && this.isPauseInStream(pause)) {
3439
+ this.idleResumeBacklog = null;
3440
+ this.appendMarker(
3441
+ VISIBILITY_CUSTOM_EVENT_TAG,
3442
+ { state: "stopped" },
3443
+ pause.pausedAtUnixMs
3444
+ );
3445
+ }
3349
3446
  this.haltCapture();
3350
3447
  await this.closeCurrent(true);
3351
3448
  if (this.uploadActive) {
@@ -3493,7 +3590,7 @@ var MobileReplayRecorder = class {
3493
3590
  }
3494
3591
  async setRoute(route) {
3495
3592
  await this.identityQueue;
3496
- if (this.running && !this.acceptingEvents) {
3593
+ if (this.running && (!this.acceptingEvents || this.isIdlePaused())) {
3497
3594
  this.route = sanitizeRoute(route);
3498
3595
  return;
3499
3596
  }
@@ -3508,6 +3605,9 @@ var MobileReplayRecorder = class {
3508
3605
  return;
3509
3606
  }
3510
3607
  this.route = next;
3608
+ if (this.sealedForIdle) {
3609
+ return;
3610
+ }
3511
3611
  this.appendCustom(
3512
3612
  ROUTE_CUSTOM_EVENT_TAG,
3513
3613
  {
@@ -3526,7 +3626,15 @@ var MobileReplayRecorder = class {
3526
3626
  if (!this.running || !this.acceptingEvents) {
3527
3627
  return;
3528
3628
  }
3529
- await this.maybeRotateSession(this.now());
3629
+ const resume = this.foreground ? this.endIdlePause(this.now()) : "none";
3630
+ await this.maybeRotateForUser(this.now());
3631
+ if (resume === "resumed") {
3632
+ this.startTimers();
3633
+ await this.captureTick(true, false);
3634
+ if (!this.running || !this.acceptingEvents) {
3635
+ return;
3636
+ }
3637
+ }
3530
3638
  const safeReason2 = truncate(
3531
3639
  (typeof reason === "string" ? reason.trim() : "") || "manual",
3532
3640
  MAX_CAPTURE_REASON_LENGTH
@@ -3542,7 +3650,13 @@ var MobileReplayRecorder = class {
3542
3650
  if (!this.running || !this.acceptingEvents) {
3543
3651
  return;
3544
3652
  }
3653
+ if (this.isIdlePaused()) {
3654
+ return;
3655
+ }
3545
3656
  await this.maybeRotateSession(this.now());
3657
+ if (this.sealedForIdle) {
3658
+ return;
3659
+ }
3546
3660
  const rawName = error instanceof Error && typeof error.name === "string" ? error.name.trim() : "";
3547
3661
  const name = SAFE_ERROR_NAMES.has(rawName) ? rawName : "Error";
3548
3662
  this.appendCustom(
@@ -3564,9 +3678,18 @@ var MobileReplayRecorder = class {
3564
3678
  if (!this.running || !this.acceptingEvents || !this.foreground) {
3565
3679
  return;
3566
3680
  }
3681
+ const input = this.noteUserInput(this.now());
3682
+ let resumeCapture = null;
3683
+ if (input === "resumed") {
3684
+ this.startTimers();
3685
+ resumeCapture = this.captureTick(true);
3686
+ }
3567
3687
  const generation = this.lifecycleGeneration;
3568
3688
  const operation = this.touchQueue.then(
3569
3689
  async () => {
3690
+ if (resumeCapture) {
3691
+ await resumeCapture;
3692
+ }
3570
3693
  await this.recordTouchAfterPrivacyCheck(sanitizedTouch, generation);
3571
3694
  }
3572
3695
  );
@@ -3593,9 +3716,9 @@ var MobileReplayRecorder = class {
3593
3716
  this.lastTouchMoveAtUnixMs = touch.timestamp;
3594
3717
  }
3595
3718
  const nowUnixMs = this.now();
3596
- if (this.rotatingSession || this.rotationReason(nowUnixMs)) {
3719
+ if (this.rotatingSession || this.sealedForIdle || this.rotationReason(nowUnixMs)) {
3597
3720
  this.diagnostic("touch-dropped-session-rotation");
3598
- void this.maybeRotateSession(nowUnixMs);
3721
+ void this.maybeRotateForUser(nowUnixMs);
3599
3722
  return;
3600
3723
  }
3601
3724
  this.appendEvent(
@@ -3620,8 +3743,9 @@ var MobileReplayRecorder = class {
3620
3743
  }
3621
3744
  }
3622
3745
  getDiagnostics() {
3746
+ const status = this.diagnosticsStatus === "recording" && this.running && this.foreground && (this.isIdlePaused() || this.sealedForIdle) ? "paused" : this.diagnosticsStatus;
3623
3747
  return {
3624
- status: this.diagnosticsStatus,
3748
+ status,
3625
3749
  sessionId: this.identity?.sessionId ?? null,
3626
3750
  recorderKind: MOBILE_RECORDER_KIND,
3627
3751
  configEpoch: this.config?.configEpoch ?? 0,
@@ -3643,9 +3767,14 @@ var MobileReplayRecorder = class {
3643
3767
  )
3644
3768
  };
3645
3769
  }
3646
- /** Current consented replay session for host trace/log correlation. */
3770
+ /**
3771
+ * Current consented replay session for host trace/log correlation. None
3772
+ * while the session has ended idle and nobody is back: a background poll
3773
+ * on an idle app is no part of a recording that ended at the user's last
3774
+ * activity.
3775
+ */
3647
3776
  getSessionId() {
3648
- return this.running && this.identityPersisted ? this.identity?.sessionId ?? null : null;
3777
+ return this.running && this.identityPersisted && !this.sealedForIdle ? this.identity?.sessionId ?? null : null;
3649
3778
  }
3650
3779
  /** Fires immediately when a session exists and again on rotate/clear. */
3651
3780
  onSessionChange(listener) {
@@ -3698,9 +3827,16 @@ var MobileReplayRecorder = class {
3698
3827
  }
3699
3828
  await this.transport?.restore();
3700
3829
  }
3701
- async captureTick(force = false) {
3830
+ /*
3831
+ * mayPauseForIdle is false only for the snapshot captureSession() takes
3832
+ * while paused, whose marker must be in before the pause check runs.
3833
+ */
3834
+ async captureTick(force = false, mayPauseForIdle = true) {
3702
3835
  const generation = this.lifecycleGeneration;
3703
- if (!this.running || !this.acceptingEvents || !this.foreground || !this.rootTag || this.captureGeneration === generation || !this.options) {
3836
+ if (!this.running || !this.acceptingEvents || !this.foreground || !this.rootTag || this.sealedForIdle || this.captureGeneration === generation || !this.options) {
3837
+ return;
3838
+ }
3839
+ if (this.isIdlePaused()) {
3704
3840
  return;
3705
3841
  }
3706
3842
  this.captureGeneration = generation;
@@ -3715,7 +3851,10 @@ var MobileReplayRecorder = class {
3715
3851
  return;
3716
3852
  }
3717
3853
  await this.maybeRotateSession(nowUnixMs);
3718
- if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation)) {
3854
+ if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.sealedForIdle) {
3855
+ return;
3856
+ }
3857
+ if (this.isIdlePaused() || this.idlePause !== null && this.rotationReason(nowUnixMs) !== null) {
3719
3858
  return;
3720
3859
  }
3721
3860
  const capturedSessionId = this.identity?.sessionId;
@@ -3724,7 +3863,6 @@ var MobileReplayRecorder = class {
3724
3863
  return;
3725
3864
  }
3726
3865
  this.touchPrivacyMap = deriveTouchPrivacyMap(tree);
3727
- const size = getViewport();
3728
3866
  const shouldForce = force || this.forceFullSnapshot || nowUnixMs - this.lastFullSnapshotAtUnixMs >= SESSION_REPLAY_CHECKOUT_INTERVAL_MS;
3729
3867
  if (shouldForce && !this.chunkBuffer.isEmpty()) {
3730
3868
  await this.closeCurrent(false);
@@ -3732,25 +3870,13 @@ var MobileReplayRecorder = class {
3732
3870
  if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.identity?.sessionId !== capturedSessionId) {
3733
3871
  return;
3734
3872
  }
3735
- const capture = this.serializer.capture(tree, {
3736
- timestamp: nowUnixMs,
3737
- url: toAppUrl(this.options.mobileAppIdentifier, this.route),
3738
- viewportWidth: size.width,
3739
- viewportHeight: size.height,
3740
- forceFullSnapshot: shouldForce
3741
- });
3742
- this.droppedEvents += capture.droppedNodes;
3743
- this.chunkBuffer.appendMany(capture.events, {
3744
- route: toAppUrl(this.options.mobileAppIdentifier, this.route),
3745
- fidelityNotices: capture.fidelityNotices
3746
- });
3747
- if (capture.hasFullSnapshot) {
3748
- this.lastFullSnapshotAtUnixMs = nowUnixMs;
3749
- this.forceFullSnapshot = false;
3750
- }
3873
+ this.appendCapture(tree, this.options, nowUnixMs, shouldForce);
3751
3874
  if (this.chunkBuffer.shouldFlush()) {
3752
3875
  await this.closeCurrent(false);
3753
3876
  }
3877
+ if (mayPauseForIdle && !this.softKeyboardVisible && nowUnixMs - this.lastInputUnixMs >= SESSION_REPLAY_IDLE_PAUSE_MS) {
3878
+ await this.pauseForIdle(nowUnixMs, generation);
3879
+ }
3754
3880
  } catch (error) {
3755
3881
  if (this.running && this.isCurrentGeneration(generation)) {
3756
3882
  this.disable("native-capture-failed", {
@@ -3764,6 +3890,183 @@ var MobileReplayRecorder = class {
3764
3890
  }
3765
3891
  }
3766
3892
  }
3893
+ appendCapture(tree, options, nowUnixMs, forceFullSnapshot) {
3894
+ const size = getViewport();
3895
+ const url = toAppUrl(options.mobileAppIdentifier, this.route);
3896
+ const capture = this.serializer.capture(tree, {
3897
+ timestamp: nowUnixMs,
3898
+ url,
3899
+ viewportWidth: size.width,
3900
+ viewportHeight: size.height,
3901
+ forceFullSnapshot
3902
+ });
3903
+ this.droppedEvents += capture.droppedNodes;
3904
+ this.chunkBuffer.appendMany(capture.events, {
3905
+ route: url,
3906
+ fidelityNotices: capture.fidelityNotices
3907
+ });
3908
+ if (capture.hasFullSnapshot) {
3909
+ this.lastFullSnapshotAtUnixMs = nowUnixMs;
3910
+ this.forceFullSnapshot = false;
3911
+ this.completeIdleResume(nowUnixMs);
3912
+ }
3913
+ }
3914
+ /*
3915
+ * The person did something: a touch, the soft keyboard showing or
3916
+ * hiding, or the app coming back to the foreground. It ends an idle
3917
+ * pause (see endIdlePause).
3918
+ */
3919
+ noteUserInput(nowUnixMs) {
3920
+ this.lastInputUnixMs = Math.max(this.lastInputUnixMs, nowUnixMs);
3921
+ return this.endIdlePause(nowUnixMs);
3922
+ }
3923
+ /*
3924
+ * What someone being back does to an idle pause, on its own for
3925
+ * captureSession(), which acts on the pause without being input.
3926
+ *
3927
+ * Inside the session's idle window the same session resumes, on the
3928
+ * snapshot of the next capture; the caller starts that capture. Once
3929
+ * the window has run out - or the session already ended idle - the
3930
+ * session is over, and the caller starts the next one through
3931
+ * maybeRotateForUser, as on any idle session; nothing changes here.
3932
+ */
3933
+ endIdlePause(nowUnixMs) {
3934
+ if (this.sealedForIdle) {
3935
+ return "session-ended";
3936
+ }
3937
+ if (!this.isIdlePaused()) {
3938
+ return "none";
3939
+ }
3940
+ if (this.rotationReason(nowUnixMs) !== null) {
3941
+ return "session-ended";
3942
+ }
3943
+ this.idleResumeBacklog = [];
3944
+ this.forceFullSnapshot = true;
3945
+ this.sessionLastActivityUnixMs = Math.max(
3946
+ this.sessionLastActivityUnixMs,
3947
+ nowUnixMs
3948
+ );
3949
+ return "resumed";
3950
+ }
3951
+ /* Paused for idle with nobody back yet: nothing is captured or recorded. */
3952
+ isIdlePaused() {
3953
+ return this.idlePause !== null && this.idleResumeBacklog === null;
3954
+ }
3955
+ /*
3956
+ * Whether the pause's marker is in the stream being recorded now, which
3957
+ * is what owes it the idle-resumed marker and dates a stop at it.
3958
+ */
3959
+ isPauseInStream(pause) {
3960
+ return pause.marked && this.identity?.sessionId === pause.sessionId && this.identity?.tabId === pause.tabId;
3961
+ }
3962
+ /*
3963
+ * Stop capturing: nobody has touched the app for the pause window.
3964
+ *
3965
+ * In this order: the idle-paused marker goes in as the last event of the
3966
+ * open chunk, that chunk goes out through the ordinary path, and the
3967
+ * sampler stops. So the last thing the recording holds before the
3968
+ * stretch nobody recorded says so - and a session the person never comes
3969
+ * back to has already ended there when its idle window runs out, so the
3970
+ * idle seal (sealForIdle) has nothing left to send.
3971
+ *
3972
+ * A recorder holding its pre-roll in memory (error-triggered, or waiting
3973
+ * for consent) has nothing to mark and nothing to send. Its pre-roll is
3974
+ * dropped instead: a trigger once the person is back must not upload a
3975
+ * minute of nobody in front of a snapshot taken after an unrecorded gap.
3976
+ */
3977
+ async pauseForIdle(nowUnixMs, generation) {
3978
+ if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.idlePause !== null || this.sealedForIdle || !this.identity) {
3979
+ return;
3980
+ }
3981
+ const paused = {
3982
+ /*
3983
+ * Never before the session began: one that started with nobody there
3984
+ * (the chunk cap rolled it over) was not idle before it existed.
3985
+ */
3986
+ idleSinceUnixMs: Math.min(
3987
+ nowUnixMs,
3988
+ Math.max(this.lastInputUnixMs, this.identity.sessionStartUnixMs)
3989
+ ),
3990
+ pausedAtUnixMs: nowUnixMs
3991
+ };
3992
+ const marked = this.uploadActive;
3993
+ this.idlePause = {
3994
+ sessionId: this.identity.sessionId,
3995
+ tabId: this.identity.tabId,
3996
+ idleSinceUnixMs: paused.idleSinceUnixMs,
3997
+ pausedAtUnixMs: paused.pausedAtUnixMs,
3998
+ marked
3999
+ };
4000
+ this.stopCaptureTimer();
4001
+ this.diagnostic("idle-paused", {
4002
+ idleForMs: nowUnixMs - paused.idleSinceUnixMs
4003
+ });
4004
+ if (!marked) {
4005
+ this.chunkBuffer.clear();
4006
+ this.rollingBuffer.clear();
4007
+ return;
4008
+ }
4009
+ this.appendMarker(IDLE_PAUSED_CUSTOM_EVENT_TAG, paused, nowUnixMs);
4010
+ await this.closeCurrent(false);
4011
+ }
4012
+ /*
4013
+ * The snapshot the session resumes on is in the buffer: the idle-resumed
4014
+ * marker goes directly behind it, then whatever was recorded since the
4015
+ * input that woke the recorder. Same session, same tab and - the pause
4016
+ * having closed its chunk - the next chunk index.
4017
+ */
4018
+ completeIdleResume(nowUnixMs) {
4019
+ const pause = this.idlePause;
4020
+ const held = this.idleResumeBacklog;
4021
+ if (pause === null || held === null) {
4022
+ return;
4023
+ }
4024
+ this.idlePause = null;
4025
+ this.idleResumeBacklog = null;
4026
+ const resumedAtUnixMs = Math.max(pause.pausedAtUnixMs, nowUnixMs);
4027
+ if (this.isPauseInStream(pause)) {
4028
+ const resumed = {
4029
+ pausedAtUnixMs: pause.pausedAtUnixMs,
4030
+ resumedAtUnixMs
4031
+ };
4032
+ this.appendMarker(
4033
+ IDLE_RESUMED_CUSTOM_EVENT_TAG,
4034
+ resumed,
4035
+ resumedAtUnixMs
4036
+ );
4037
+ }
4038
+ this.releaseHeldEvents(held);
4039
+ this.diagnostic("idle-resumed", {
4040
+ pausedForMs: resumedAtUnixMs - pause.pausedAtUnixMs
4041
+ });
4042
+ }
4043
+ /*
4044
+ * A marker the recorder writes about itself - the idle pause and resume,
4045
+ * and a stop while paused - straight into the open chunk, stamped with
4046
+ * the moment it stands for. None of them is activity (the idle rollover
4047
+ * must not move for them), and none is "now": a stop while paused is
4048
+ * dated at the pause.
4049
+ */
4050
+ appendMarker(tag, payload, timestamp) {
4051
+ const options = {};
4052
+ if (this.options) {
4053
+ options.route = toAppUrl(this.options.mobileAppIdentifier, this.route);
4054
+ }
4055
+ if (!this.chunkBuffer.append(
4056
+ createCustomEvent(tag, payload, timestamp),
4057
+ options
4058
+ )) {
4059
+ this.droppedEvents += 1;
4060
+ }
4061
+ }
4062
+ /* What was held for a resume, in the order it was recorded. */
4063
+ releaseHeldEvents(held) {
4064
+ for (const entry of held) {
4065
+ if (!this.chunkBuffer.append(entry.event, entry.options)) {
4066
+ this.droppedEvents += 1;
4067
+ }
4068
+ }
4069
+ }
3767
4070
  appendCustom(tag, payload, signal, allowWhenQuiesced = false) {
3768
4071
  this.appendEvent(
3769
4072
  createCustomEvent(tag, payload, this.now()),
@@ -3775,15 +4078,17 @@ var MobileReplayRecorder = class {
3775
4078
  if (!this.running || !this.acceptingEvents && !allowWhenQuiesced) {
3776
4079
  return;
3777
4080
  }
4081
+ if (this.sealedForIdle) {
4082
+ return;
4083
+ }
3778
4084
  if (!Number.isFinite(event.timestamp)) {
3779
4085
  this.droppedEvents += 1;
3780
4086
  this.diagnostic("invalid-event-timestamp");
3781
4087
  return;
3782
4088
  }
3783
- this.sessionLastActivityUnixMs = Math.max(
3784
- this.sessionLastActivityUnixMs,
3785
- event.timestamp
3786
- );
4089
+ if (this.isIdlePaused()) {
4090
+ return;
4091
+ }
3787
4092
  const appendOptions = {};
3788
4093
  if (this.options) {
3789
4094
  appendOptions.route = toAppUrl(
@@ -3794,6 +4099,18 @@ var MobileReplayRecorder = class {
3794
4099
  if (signal) {
3795
4100
  appendOptions.signal = signal;
3796
4101
  }
4102
+ if (this.idleResumeBacklog !== null) {
4103
+ if (this.idleResumeBacklog.length < MAX_EVENTS_HELD_FOR_RESUME) {
4104
+ this.idleResumeBacklog.push({ event, options: appendOptions });
4105
+ } else {
4106
+ this.droppedEvents += 1;
4107
+ }
4108
+ return;
4109
+ }
4110
+ this.sessionLastActivityUnixMs = Math.max(
4111
+ this.sessionLastActivityUnixMs,
4112
+ event.timestamp
4113
+ );
3797
4114
  const accepted = this.chunkBuffer.append(event, appendOptions);
3798
4115
  if (!accepted) {
3799
4116
  this.droppedEvents += 1;
@@ -3803,7 +4120,7 @@ var MobileReplayRecorder = class {
3803
4120
  }
3804
4121
  }
3805
4122
  async closeCurrent(isFinal) {
3806
- const closesFinalChunkSlot = !isFinal && !this.rotatingSession && this.uploadActive && this.sessionStore?.getNextChunkIndex() === MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1;
4123
+ const closesFinalChunkSlot = !isFinal && !this.rotatingSession && !this.sealedForIdle && this.uploadActive && this.sessionStore?.getNextChunkIndex() === MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1;
3807
4124
  if (closesFinalChunkSlot) {
3808
4125
  this.rotatingSession = true;
3809
4126
  const rotationOptions = {};
@@ -3815,7 +4132,7 @@ var MobileReplayRecorder = class {
3815
4132
  }
3816
4133
  this.chunkBuffer.append(
3817
4134
  createCustomEvent(
3818
- "oneuptime.session-rotated",
4135
+ SESSION_ROTATED_CUSTOM_EVENT_TAG,
3819
4136
  { reason: "chunk-cap" },
3820
4137
  this.now()
3821
4138
  ),
@@ -3979,6 +4296,63 @@ var MobileReplayRecorder = class {
3979
4296
  });
3980
4297
  }
3981
4298
  );
4299
+ this.installKeyboardListeners();
4300
+ }
4301
+ installKeyboardListeners() {
4302
+ this.removeKeyboardListeners();
4303
+ const keyboard = this.keyboard;
4304
+ this.softKeyboardVisible = false;
4305
+ if (!keyboard || typeof keyboard.addListener !== "function") {
4306
+ return;
4307
+ }
4308
+ try {
4309
+ this.softKeyboardVisible = keyboard.isVisible?.() === true;
4310
+ } catch {
4311
+ this.softKeyboardVisible = false;
4312
+ }
4313
+ try {
4314
+ this.keyboardSubscriptions.push(
4315
+ keyboard.addListener("keyboardDidShow", () => {
4316
+ this.onKeyboardChange(true);
4317
+ }),
4318
+ keyboard.addListener("keyboardDidHide", () => {
4319
+ this.onKeyboardChange(false);
4320
+ })
4321
+ );
4322
+ } catch {
4323
+ this.removeKeyboardListeners();
4324
+ this.softKeyboardVisible = false;
4325
+ this.diagnostic("keyboard-subscribe-failed");
4326
+ }
4327
+ }
4328
+ removeKeyboardListeners() {
4329
+ const subscriptions = this.keyboardSubscriptions;
4330
+ this.keyboardSubscriptions = [];
4331
+ for (const subscription of subscriptions) {
4332
+ try {
4333
+ subscription.remove();
4334
+ } catch {
4335
+ }
4336
+ }
4337
+ }
4338
+ /*
4339
+ * The soft keyboard came up or went away. That is the person - a field
4340
+ * tapped, a message sent - so it counts as input and resumes a paused
4341
+ * recording the way a touch does; and while it is up the idle pause
4342
+ * waits, since what is typed on it reaches no view the recorder sees.
4343
+ */
4344
+ onKeyboardChange(visible) {
4345
+ this.softKeyboardVisible = visible;
4346
+ if (!this.running || !this.acceptingEvents || !this.foreground) {
4347
+ return;
4348
+ }
4349
+ const input = this.noteUserInput(this.now());
4350
+ if (input === "resumed") {
4351
+ this.startTimers();
4352
+ void this.captureTick(true);
4353
+ } else if (input === "session-ended") {
4354
+ void this.maybeRotateForUser(this.now());
4355
+ }
3982
4356
  }
3983
4357
  async onAppStateChange(state, generation) {
3984
4358
  if (!this.running || !this.isCurrentGeneration(generation)) {
@@ -3998,6 +4372,13 @@ var MobileReplayRecorder = class {
3998
4372
  if (!this.running || !this.isCurrentGeneration(generation)) {
3999
4373
  return;
4000
4374
  }
4375
+ if (this.idlePause !== null || this.sealedForIdle) {
4376
+ this.idleResumeBacklog = null;
4377
+ if (this.uploadActive) {
4378
+ await this.transport?.drain();
4379
+ }
4380
+ return;
4381
+ }
4001
4382
  this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "hidden" });
4002
4383
  await this.closeCurrent(false);
4003
4384
  if (this.uploadActive) {
@@ -4007,6 +4388,7 @@ var MobileReplayRecorder = class {
4007
4388
  }
4008
4389
  if (isActive && !this.foreground) {
4009
4390
  this.foreground = true;
4391
+ const input = this.noteUserInput(this.now());
4010
4392
  const policyReady = await this.refreshRuntimePolicy(
4011
4393
  generation,
4012
4394
  true,
@@ -4015,15 +4397,22 @@ var MobileReplayRecorder = class {
4015
4397
  if (!policyReady) {
4016
4398
  return;
4017
4399
  }
4018
- await this.maybeRotateSession(this.now());
4400
+ const sessionIdBeforeResume = this.identity?.sessionId;
4401
+ await this.maybeRotateSession(this.now(), true);
4019
4402
  if (!this.running || !this.isCurrentGeneration(generation)) {
4020
4403
  return;
4021
4404
  }
4022
- this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "visible" });
4023
- this.forceFullSnapshot = true;
4405
+ const resumedPausedSession = input === "resumed" && this.identity?.sessionId === sessionIdBeforeResume;
4406
+ if (!resumedPausedSession) {
4407
+ this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "visible" });
4408
+ }
4409
+ const openedOnSnapshot = this.identity?.sessionId !== sessionIdBeforeResume && this.lastFullSnapshotAtUnixMs > 0;
4410
+ if (!openedOnSnapshot) {
4411
+ this.forceFullSnapshot = true;
4412
+ }
4024
4413
  this.startTimers();
4025
4414
  this.diagnosticsStatus = this.consentState() === "Unknown" ? "consent-required" : "recording";
4026
- await this.captureTick(true);
4415
+ await this.captureTick(!openedOnSnapshot);
4027
4416
  if (this.uploadActive && this.running) {
4028
4417
  this.transport?.resume();
4029
4418
  }
@@ -4042,16 +4431,31 @@ var MobileReplayRecorder = class {
4042
4431
  void this.captureTick();
4043
4432
  }, intervalMs);
4044
4433
  this.flushTimer = setInterval(() => {
4434
+ if (this.isIdlePaused()) {
4435
+ if (this.acceptingEvents) {
4436
+ void this.maybeRotateSession(this.now());
4437
+ }
4438
+ return;
4439
+ }
4045
4440
  if (this.acceptingEvents) {
4046
4441
  void this.closeCurrent(false);
4047
4442
  }
4048
4443
  }, SESSION_REPLAY_FLUSH_INTERVAL_MS);
4049
4444
  }
4050
- clearTimers() {
4445
+ /*
4446
+ * The sampler alone. An idle pause stops it - every tick is a native
4447
+ * traversal of the whole view tree, which is what a pause is for not
4448
+ * paying - and keeps the 15-second flush timer as its heartbeat, rather
4449
+ * than waking the JS thread twice a second to find nothing to do.
4450
+ */
4451
+ stopCaptureTimer() {
4051
4452
  if (this.captureTimer) {
4052
4453
  clearInterval(this.captureTimer);
4053
4454
  this.captureTimer = null;
4054
4455
  }
4456
+ }
4457
+ clearTimers() {
4458
+ this.stopCaptureTimer();
4055
4459
  if (this.flushTimer) {
4056
4460
  clearInterval(this.flushTimer);
4057
4461
  this.flushTimer = null;
@@ -4143,6 +4547,7 @@ var MobileReplayRecorder = class {
4143
4547
  this.clearTimers();
4144
4548
  this.appStateSubscription?.remove();
4145
4549
  this.appStateSubscription = null;
4550
+ this.removeKeyboardListeners();
4146
4551
  this.stopConnectivity();
4147
4552
  this.restoreGlobalErrorHandler();
4148
4553
  }
@@ -4303,9 +4708,16 @@ var MobileReplayRecorder = class {
4303
4708
  if (!this.running || identityEpoch !== this.identityCancellationEpoch) {
4304
4709
  return;
4305
4710
  }
4711
+ if (!this.rotationPromise && !this.sealedForIdle) {
4712
+ if (this.isIdlePaused()) {
4713
+ await this.sealForIdleNow();
4714
+ } else if (this.rotationReason(this.now()) === "idle") {
4715
+ await this.maybeRotateSession(this.now());
4716
+ }
4717
+ }
4306
4718
  if (this.rotationPromise) {
4307
4719
  await this.rotationPromise;
4308
- } else {
4720
+ } else if (!this.sealedForIdle) {
4309
4721
  this.rotatingSession = true;
4310
4722
  const rotation = this.performSessionRotation(
4311
4723
  this.now(),
@@ -4640,16 +5052,24 @@ var MobileReplayRecorder = class {
4640
5052
  chunkIndex: 0
4641
5053
  };
4642
5054
  }
5055
+ /*
5056
+ * Idle before the duration cap, the order Common's SessionIdentity decides
5057
+ * them in. A session left for the idle window ended where it was left,
5058
+ * even when it has outlived the cap by the time anything notices, and
5059
+ * only an idle rotation dates its end there (see performSessionRotation):
5060
+ * called "duration", a session three hours old put in the background for
5061
+ * two more was sealed two hours after its footage.
5062
+ */
4643
5063
  rotationReason(nowUnixMs) {
4644
5064
  if (!this.identity) {
4645
5065
  return null;
4646
5066
  }
4647
- if (nowUnixMs - this.identity.sessionStartUnixMs >= SESSION_REPLAY_MAX_SESSION_MS) {
4648
- return "duration";
4649
- }
4650
5067
  if (nowUnixMs - this.sessionLastActivityUnixMs >= SESSION_REPLAY_IDLE_ROLLOVER_MS) {
4651
5068
  return "idle";
4652
5069
  }
5070
+ if (nowUnixMs - this.identity.sessionStartUnixMs >= SESSION_REPLAY_MAX_SESSION_MS) {
5071
+ return "duration";
5072
+ }
4653
5073
  return null;
4654
5074
  }
4655
5075
  runExternalActivity(nowUnixMs, action) {
@@ -4684,23 +5104,61 @@ var MobileReplayRecorder = class {
4684
5104
  }
4685
5105
  });
4686
5106
  }
4687
- async maybeRotateSession(nowUnixMs) {
4688
- if (this.rotationPromise) {
5107
+ /*
5108
+ * isUserBack: the caller knows the user is here - a touch, the app
5109
+ * returning to the foreground, an explicit captureSession(). An idle
5110
+ * session then rotates at once. Anything else (a capture tick, a route
5111
+ * change, a developer event) only finds out the session went idle, and
5112
+ * seals it to wait for them (see sealedForIdle).
5113
+ */
5114
+ async maybeRotateSession(nowUnixMs, isUserBack = false) {
5115
+ while (this.rotationPromise) {
4689
5116
  await this.rotationPromise;
4690
- return;
5117
+ if (!isUserBack || !this.sealedForIdle) {
5118
+ return;
5119
+ }
4691
5120
  }
4692
5121
  if (!this.running || this.rotatingSession || !this.identity || !this.config || !this.sessionStore || !this.options) {
4693
5122
  return;
4694
5123
  }
4695
- const reason = this.rotationReason(nowUnixMs);
5124
+ let reason;
5125
+ if (this.sealedForIdle) {
5126
+ if (!isUserBack) {
5127
+ return;
5128
+ }
5129
+ reason = "idle";
5130
+ } else {
5131
+ reason = this.rotationReason(nowUnixMs);
5132
+ if (reason !== null && this.isIdlePaused()) {
5133
+ reason = "idle";
5134
+ }
5135
+ }
4696
5136
  if (!reason) {
4697
5137
  return;
4698
5138
  }
4699
5139
  this.rotatingSession = true;
4700
- const operation = this.performSessionRotation(
4701
- nowUnixMs,
4702
- reason
4703
- );
5140
+ const operation = reason === "idle" && !isUserBack ? this.sealForIdle() : this.performSessionRotation(nowUnixMs, reason);
5141
+ this.rotationPromise = operation;
5142
+ try {
5143
+ await operation;
5144
+ } finally {
5145
+ if (this.rotationPromise === operation) {
5146
+ this.rotationPromise = null;
5147
+ }
5148
+ this.rotatingSession = false;
5149
+ }
5150
+ }
5151
+ /*
5152
+ * The idle seal on demand, under the lock every rotation takes: for an
5153
+ * identity change on a session paused for idle, which no clock has ended
5154
+ * yet but nobody is in.
5155
+ */
5156
+ async sealForIdleNow() {
5157
+ if (this.rotationPromise || this.rotatingSession || this.sealedForIdle) {
5158
+ return;
5159
+ }
5160
+ this.rotatingSession = true;
5161
+ const operation = this.sealForIdle();
4704
5162
  this.rotationPromise = operation;
4705
5163
  try {
4706
5164
  await operation;
@@ -4711,8 +5169,68 @@ var MobileReplayRecorder = class {
4711
5169
  this.rotatingSession = false;
4712
5170
  }
4713
5171
  }
5172
+ /*
5173
+ * maybeRotateSession for a user who is here. On an app whose session
5174
+ * ended idle - or was paused for idle, which stops the capture tick too
5175
+ * (#4208) - the policy is brought up to date first: no capture tick has
5176
+ * refreshed it while the app sat idle, and the next session must not open
5177
+ * under one that has since changed (or turned replay off).
5178
+ */
5179
+ async maybeRotateForUser(nowUnixMs) {
5180
+ if ((this.sealedForIdle || this.idlePause !== null) && !this.rotatingSession) {
5181
+ const policyReady = await this.refreshRuntimePolicy(
5182
+ this.lifecycleGeneration,
5183
+ false,
5184
+ "periodic"
5185
+ );
5186
+ if (!policyReady) {
5187
+ return;
5188
+ }
5189
+ }
5190
+ await this.maybeRotateSession(nowUnixMs, true);
5191
+ }
5192
+ /*
5193
+ * End the session as idle, where it really ended, and wait for the user.
5194
+ *
5195
+ * Sealed with whatever footage is still buffered - dated by that footage,
5196
+ * never by now - and with no marker of its own: a marker stamped now
5197
+ * would date the session's end half an hour or more after anything the
5198
+ * player can show. With nothing buffered nothing is sent, and the server
5199
+ * ends the session at its last chunk - for a session paused for idle
5200
+ * (#4208), the chunk its idle-paused marker closed.
5201
+ */
5202
+ async sealForIdle() {
5203
+ this.sealedForIdle = true;
5204
+ this.idlePause = null;
5205
+ this.idleResumeBacklog = null;
5206
+ this.clearTimers();
5207
+ this.diagnostic("session-ended-idle", {
5208
+ idleForMs: Math.max(0, this.now() - this.sessionLastActivityUnixMs)
5209
+ });
5210
+ this.notifySessionChange();
5211
+ await this.closeCurrent(true);
5212
+ await this.closeQueue;
5213
+ if (!this.running) {
5214
+ return;
5215
+ }
5216
+ if (this.uploadActive) {
5217
+ await this.transport?.drain();
5218
+ } else {
5219
+ this.rollingBuffer.clear();
5220
+ }
5221
+ if (this.running && this.identityPersisted) {
5222
+ await this.sessionStore?.end();
5223
+ }
5224
+ }
4714
5225
  async performSessionRotation(nowUnixMs, reason) {
4715
- this.appendCustom("oneuptime.session-rotated", { reason }, void 0, true);
5226
+ if (reason !== "idle") {
5227
+ this.appendCustom(
5228
+ SESSION_ROTATED_CUSTOM_EVENT_TAG,
5229
+ { reason },
5230
+ void 0,
5231
+ true
5232
+ );
5233
+ }
4716
5234
  await this.closeCurrent(true);
4717
5235
  await this.closeQueue;
4718
5236
  if (!this.running) {
@@ -4727,12 +5245,66 @@ var MobileReplayRecorder = class {
4727
5245
  return;
4728
5246
  }
4729
5247
  await this.completeSessionIdentityRotation(nowUnixMs, reason);
5248
+ if (reason === "idle" && this.running) {
5249
+ await this.openSessionAfterIdle();
5250
+ }
5251
+ }
5252
+ /*
5253
+ * The first things a session after an idle one records: a full snapshot
5254
+ * of the screen the user came back to, then the rotation marker - the
5255
+ * snapshot first, so chunk 0 is a seek anchor rather than a lone marker.
5256
+ *
5257
+ * Taken here, inside the rotation, rather than left to the next capture
5258
+ * tick: until the rotation settles, touches are dropped and every other
5259
+ * event waits for it (rotatingSession), so nothing can land ahead of the
5260
+ * snapshot. A user who has just come back is touching the screen, and
5261
+ * half a second of that was otherwise a chunk 0 with no screen in it.
5262
+ *
5263
+ * In the background or without a registered root there is no screen to
5264
+ * capture yet; the marker opens the session alone.
5265
+ */
5266
+ async openSessionAfterIdle() {
5267
+ const generation = this.lifecycleGeneration;
5268
+ const rootTag = this.rootTag;
5269
+ const options = this.options;
5270
+ const sessionId = this.identity?.sessionId;
5271
+ if (this.foreground && rootTag && options && sessionId) {
5272
+ const capturedAtUnixMs = this.now();
5273
+ let tree;
5274
+ try {
5275
+ tree = await this.nativeViewTree.captureViewTree(rootTag);
5276
+ } catch (error) {
5277
+ if (this.running && this.isCurrentGeneration(generation)) {
5278
+ this.disable("native-capture-failed", {
5279
+ name: error instanceof Error ? error.name : "Error"
5280
+ });
5281
+ this.haltCapture();
5282
+ }
5283
+ return;
5284
+ }
5285
+ if (!this.running || !this.isCurrentGeneration(generation) || this.identity?.sessionId !== sessionId) {
5286
+ return;
5287
+ }
5288
+ if (this.foreground) {
5289
+ this.touchPrivacyMap = deriveTouchPrivacyMap(tree);
5290
+ this.appendCapture(tree, options, capturedAtUnixMs, true);
5291
+ }
5292
+ }
5293
+ this.appendCustom(
5294
+ SESSION_ROTATED_CUSTOM_EVENT_TAG,
5295
+ { reason: "idle" },
5296
+ void 0,
5297
+ true
5298
+ );
4730
5299
  }
4731
5300
  async completeSessionIdentityRotation(nowUnixMs, reason) {
4732
5301
  if (!this.sessionStore || !this.config || !this.options) {
4733
5302
  return;
4734
5303
  }
4735
5304
  this.identity = this.identityPersisted ? await this.sessionStore.rotate(nowUnixMs) : this.ephemeralIdentity(nowUnixMs);
5305
+ this.sealedForIdle = false;
5306
+ this.idlePause = null;
5307
+ this.idleResumeBacklog = null;
4736
5308
  this.notifySessionChange();
4737
5309
  this.sampled = this.identityPersisted ? deterministicSample(
4738
5310
  this.identity.sessionId,
@@ -4753,6 +5325,10 @@ var MobileReplayRecorder = class {
4753
5325
  if (this.config.captureTrigger === "Always" /* Always */ && this.identityPersisted && !this.sampled && !this.config.isTargeted) {
4754
5326
  this.disable("not-sampled");
4755
5327
  this.haltCapture();
5328
+ return;
5329
+ }
5330
+ if (this.captureTimer === null && this.running && this.foreground) {
5331
+ this.startTimers();
4756
5332
  }
4757
5333
  }
4758
5334
  disable(code, details) {