@oneuptime/react-native-replay 14.0.10 → 14.0.12
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/README.md +8 -2
- package/dist/index.cjs +217 -49
- package/dist/index.cjs.map +2 -2
- package/dist/index.js +217 -49
- package/dist/index.js.map +2 -2
- package/dist/types/Contract.d.ts +1 -0
- package/dist/types/Contract.d.ts.map +1 -1
- package/dist/types/MobileReplayRecorder.d.ts +11 -1
- package/dist/types/MobileReplayRecorder.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -196,8 +196,8 @@ Pass `debug: true` at startup to mirror those codes to the development console.
|
|
|
196
196
|
|
|
197
197
|
Use the current consented session ID to correlate the application's own traces
|
|
198
198
|
and logs with replay. The listener fires immediately when a session exists,
|
|
199
|
-
again on every rotation, and with `null` when capture stops
|
|
200
|
-
withdrawn:
|
|
199
|
+
again on every rotation, and with `null` when capture stops, consent is
|
|
200
|
+
withdrawn, or the session ends idle:
|
|
201
201
|
|
|
202
202
|
```ts
|
|
203
203
|
const unsubscribe = OneUptimeReplay.onSessionChange((sessionId) => {
|
|
@@ -221,6 +221,12 @@ server response. AppState backgrounding records visibility, closes the current
|
|
|
221
221
|
chunk, drains the outbox only when policy and consent permit uploading, and
|
|
222
222
|
pauses sampling; foregrounding resumes with a fresh full snapshot.
|
|
223
223
|
|
|
224
|
+
Thirty minutes with no touch and no SDK call ends the session at that last
|
|
225
|
+
activity, so time in the background is never added to the end of a session.
|
|
226
|
+
An app left open in the foreground stops sampling then rather than starting a
|
|
227
|
+
session every half hour. The user's next touch, a return to the foreground,
|
|
228
|
+
or `captureSession()` starts a new session on a fresh full snapshot.
|
|
229
|
+
|
|
224
230
|
Policy is revalidated without HTTP-cache reuse before every foreground resume,
|
|
225
231
|
after identity changes, and at five-minute intervals while the app remains
|
|
226
232
|
active. An explicit disable takes effect before more capture; a transient
|
package/dist/index.cjs
CHANGED
|
@@ -98,7 +98,7 @@ var SESSION_REPLAY_MAX_OFFLINE_DELAY_MS = 3 * 24 * 60 * 60 * 1e3;
|
|
|
98
98
|
var MOBILE_CAPTURE_INTERVAL_MS = 500;
|
|
99
99
|
var MOBILE_RECORDER_KIND = "rn-view-tree";
|
|
100
100
|
var SYNTHETIC_RRWEB_VERSION = "synthetic-1";
|
|
101
|
-
var RECORDER_VERSION = true ? "14.0.
|
|
101
|
+
var RECORDER_VERSION = true ? "14.0.12" : "0.0.0-dev";
|
|
102
102
|
var SESSION_REPLAY_MOBILE_RECORDER_CAPABILITIES = [
|
|
103
103
|
"mobile-view-tree",
|
|
104
104
|
"mobile-touch-events",
|
|
@@ -150,6 +150,7 @@ var IDENTIFY_CUSTOM_EVENT_TAG = "oneuptime.identify";
|
|
|
150
150
|
var TAGS_CUSTOM_EVENT_TAG = "oneuptime.tags";
|
|
151
151
|
var VISIBILITY_CUSTOM_EVENT_TAG = "oneuptime.visibility";
|
|
152
152
|
var TOUCH_CUSTOM_EVENT_TAG = "oneuptime.touch";
|
|
153
|
+
var SESSION_ROTATED_CUSTOM_EVENT_TAG = "oneuptime.session-rotated";
|
|
153
154
|
|
|
154
155
|
// src/Sanitize.ts
|
|
155
156
|
var MAX_TRAIT_KEYS = 20;
|
|
@@ -3120,6 +3121,19 @@ var MobileReplayRecorder = class {
|
|
|
3120
3121
|
__publicField(this, "pendingStartCancellation", null);
|
|
3121
3122
|
__publicField(this, "identityPersisted", false);
|
|
3122
3123
|
__publicField(this, "sessionLastActivityUnixMs", 0);
|
|
3124
|
+
/*
|
|
3125
|
+
* The session went the idle window without activity, was sealed where it
|
|
3126
|
+
* ended, and nobody has come back since.
|
|
3127
|
+
*
|
|
3128
|
+
* Rotating on the spot instead (the old behaviour) started a session
|
|
3129
|
+
* nobody was in: an app left open in the foreground with the screen on
|
|
3130
|
+
* re-snapshots every minute, so it filed a new session every half hour
|
|
3131
|
+
* for as long as it stayed open. Now nothing is captured, uploaded or
|
|
3132
|
+
* handed to the host as a session id until the user is back - a touch,
|
|
3133
|
+
* the app returning to the foreground, or an explicit captureSession() -
|
|
3134
|
+
* and that starts the next session (see maybeRotateSession).
|
|
3135
|
+
*/
|
|
3136
|
+
__publicField(this, "sealedForIdle", false);
|
|
3123
3137
|
__publicField(this, "rotatingSession", false);
|
|
3124
3138
|
__publicField(this, "rotationPromise", null);
|
|
3125
3139
|
__publicField(this, "appStateQueue", Promise.resolve());
|
|
@@ -3207,6 +3221,7 @@ var MobileReplayRecorder = class {
|
|
|
3207
3221
|
this.appStateQueue = Promise.resolve();
|
|
3208
3222
|
this.rotationPromise = null;
|
|
3209
3223
|
this.rotatingSession = false;
|
|
3224
|
+
this.sealedForIdle = false;
|
|
3210
3225
|
this.policyRefreshPromise = null;
|
|
3211
3226
|
this.lastPolicyRefreshAtUnixMs = 0;
|
|
3212
3227
|
this.forceFullSnapshot = true;
|
|
@@ -3379,12 +3394,14 @@ var MobileReplayRecorder = class {
|
|
|
3379
3394
|
this.diagnosticsStatus = "stopped";
|
|
3380
3395
|
return;
|
|
3381
3396
|
}
|
|
3382
|
-
this.
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3397
|
+
if (this.rotationReason(this.now()) !== "idle") {
|
|
3398
|
+
this.appendCustom(
|
|
3399
|
+
VISIBILITY_CUSTOM_EVENT_TAG,
|
|
3400
|
+
{ state: "stopped" },
|
|
3401
|
+
void 0,
|
|
3402
|
+
true
|
|
3403
|
+
);
|
|
3404
|
+
}
|
|
3388
3405
|
this.haltCapture();
|
|
3389
3406
|
await this.closeCurrent(true);
|
|
3390
3407
|
if (this.uploadActive) {
|
|
@@ -3547,6 +3564,9 @@ var MobileReplayRecorder = class {
|
|
|
3547
3564
|
return;
|
|
3548
3565
|
}
|
|
3549
3566
|
this.route = next;
|
|
3567
|
+
if (this.sealedForIdle) {
|
|
3568
|
+
return;
|
|
3569
|
+
}
|
|
3550
3570
|
this.appendCustom(
|
|
3551
3571
|
ROUTE_CUSTOM_EVENT_TAG,
|
|
3552
3572
|
{
|
|
@@ -3565,7 +3585,7 @@ var MobileReplayRecorder = class {
|
|
|
3565
3585
|
if (!this.running || !this.acceptingEvents) {
|
|
3566
3586
|
return;
|
|
3567
3587
|
}
|
|
3568
|
-
await this.
|
|
3588
|
+
await this.maybeRotateForUser(this.now());
|
|
3569
3589
|
const safeReason2 = truncate(
|
|
3570
3590
|
(typeof reason === "string" ? reason.trim() : "") || "manual",
|
|
3571
3591
|
MAX_CAPTURE_REASON_LENGTH
|
|
@@ -3582,6 +3602,9 @@ var MobileReplayRecorder = class {
|
|
|
3582
3602
|
return;
|
|
3583
3603
|
}
|
|
3584
3604
|
await this.maybeRotateSession(this.now());
|
|
3605
|
+
if (this.sealedForIdle) {
|
|
3606
|
+
return;
|
|
3607
|
+
}
|
|
3585
3608
|
const rawName = error instanceof Error && typeof error.name === "string" ? error.name.trim() : "";
|
|
3586
3609
|
const name = SAFE_ERROR_NAMES.has(rawName) ? rawName : "Error";
|
|
3587
3610
|
this.appendCustom(
|
|
@@ -3632,9 +3655,9 @@ var MobileReplayRecorder = class {
|
|
|
3632
3655
|
this.lastTouchMoveAtUnixMs = touch.timestamp;
|
|
3633
3656
|
}
|
|
3634
3657
|
const nowUnixMs = this.now();
|
|
3635
|
-
if (this.rotatingSession || this.rotationReason(nowUnixMs)) {
|
|
3658
|
+
if (this.rotatingSession || this.sealedForIdle || this.rotationReason(nowUnixMs)) {
|
|
3636
3659
|
this.diagnostic("touch-dropped-session-rotation");
|
|
3637
|
-
void this.
|
|
3660
|
+
void this.maybeRotateForUser(nowUnixMs);
|
|
3638
3661
|
return;
|
|
3639
3662
|
}
|
|
3640
3663
|
this.appendEvent(
|
|
@@ -3682,9 +3705,14 @@ var MobileReplayRecorder = class {
|
|
|
3682
3705
|
)
|
|
3683
3706
|
};
|
|
3684
3707
|
}
|
|
3685
|
-
/**
|
|
3708
|
+
/**
|
|
3709
|
+
* Current consented replay session for host trace/log correlation. None
|
|
3710
|
+
* while the session has ended idle and nobody is back: a background poll
|
|
3711
|
+
* on an idle app is no part of a recording that ended at the user's last
|
|
3712
|
+
* activity.
|
|
3713
|
+
*/
|
|
3686
3714
|
getSessionId() {
|
|
3687
|
-
return this.running && this.identityPersisted ? this.identity?.sessionId ?? null : null;
|
|
3715
|
+
return this.running && this.identityPersisted && !this.sealedForIdle ? this.identity?.sessionId ?? null : null;
|
|
3688
3716
|
}
|
|
3689
3717
|
/** Fires immediately when a session exists and again on rotate/clear. */
|
|
3690
3718
|
onSessionChange(listener) {
|
|
@@ -3739,7 +3767,7 @@ var MobileReplayRecorder = class {
|
|
|
3739
3767
|
}
|
|
3740
3768
|
async captureTick(force = false) {
|
|
3741
3769
|
const generation = this.lifecycleGeneration;
|
|
3742
|
-
if (!this.running || !this.acceptingEvents || !this.foreground || !this.rootTag || this.captureGeneration === generation || !this.options) {
|
|
3770
|
+
if (!this.running || !this.acceptingEvents || !this.foreground || !this.rootTag || this.sealedForIdle || this.captureGeneration === generation || !this.options) {
|
|
3743
3771
|
return;
|
|
3744
3772
|
}
|
|
3745
3773
|
this.captureGeneration = generation;
|
|
@@ -3754,7 +3782,7 @@ var MobileReplayRecorder = class {
|
|
|
3754
3782
|
return;
|
|
3755
3783
|
}
|
|
3756
3784
|
await this.maybeRotateSession(nowUnixMs);
|
|
3757
|
-
if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation)) {
|
|
3785
|
+
if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.sealedForIdle) {
|
|
3758
3786
|
return;
|
|
3759
3787
|
}
|
|
3760
3788
|
const capturedSessionId = this.identity?.sessionId;
|
|
@@ -3763,7 +3791,6 @@ var MobileReplayRecorder = class {
|
|
|
3763
3791
|
return;
|
|
3764
3792
|
}
|
|
3765
3793
|
this.touchPrivacyMap = deriveTouchPrivacyMap(tree);
|
|
3766
|
-
const size = getViewport();
|
|
3767
3794
|
const shouldForce = force || this.forceFullSnapshot || nowUnixMs - this.lastFullSnapshotAtUnixMs >= SESSION_REPLAY_CHECKOUT_INTERVAL_MS;
|
|
3768
3795
|
if (shouldForce && !this.chunkBuffer.isEmpty()) {
|
|
3769
3796
|
await this.closeCurrent(false);
|
|
@@ -3771,22 +3798,7 @@ var MobileReplayRecorder = class {
|
|
|
3771
3798
|
if (!this.running || !this.acceptingEvents || !this.foreground || !this.isCurrentGeneration(generation) || this.identity?.sessionId !== capturedSessionId) {
|
|
3772
3799
|
return;
|
|
3773
3800
|
}
|
|
3774
|
-
|
|
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
|
-
}
|
|
3801
|
+
this.appendCapture(tree, this.options, nowUnixMs, shouldForce);
|
|
3790
3802
|
if (this.chunkBuffer.shouldFlush()) {
|
|
3791
3803
|
await this.closeCurrent(false);
|
|
3792
3804
|
}
|
|
@@ -3803,6 +3815,26 @@ var MobileReplayRecorder = class {
|
|
|
3803
3815
|
}
|
|
3804
3816
|
}
|
|
3805
3817
|
}
|
|
3818
|
+
appendCapture(tree, options, nowUnixMs, forceFullSnapshot) {
|
|
3819
|
+
const size = getViewport();
|
|
3820
|
+
const url = toAppUrl(options.mobileAppIdentifier, this.route);
|
|
3821
|
+
const capture = this.serializer.capture(tree, {
|
|
3822
|
+
timestamp: nowUnixMs,
|
|
3823
|
+
url,
|
|
3824
|
+
viewportWidth: size.width,
|
|
3825
|
+
viewportHeight: size.height,
|
|
3826
|
+
forceFullSnapshot
|
|
3827
|
+
});
|
|
3828
|
+
this.droppedEvents += capture.droppedNodes;
|
|
3829
|
+
this.chunkBuffer.appendMany(capture.events, {
|
|
3830
|
+
route: url,
|
|
3831
|
+
fidelityNotices: capture.fidelityNotices
|
|
3832
|
+
});
|
|
3833
|
+
if (capture.hasFullSnapshot) {
|
|
3834
|
+
this.lastFullSnapshotAtUnixMs = nowUnixMs;
|
|
3835
|
+
this.forceFullSnapshot = false;
|
|
3836
|
+
}
|
|
3837
|
+
}
|
|
3806
3838
|
appendCustom(tag, payload, signal, allowWhenQuiesced = false) {
|
|
3807
3839
|
this.appendEvent(
|
|
3808
3840
|
createCustomEvent(tag, payload, this.now()),
|
|
@@ -3814,6 +3846,9 @@ var MobileReplayRecorder = class {
|
|
|
3814
3846
|
if (!this.running || !this.acceptingEvents && !allowWhenQuiesced) {
|
|
3815
3847
|
return;
|
|
3816
3848
|
}
|
|
3849
|
+
if (this.sealedForIdle) {
|
|
3850
|
+
return;
|
|
3851
|
+
}
|
|
3817
3852
|
if (!Number.isFinite(event.timestamp)) {
|
|
3818
3853
|
this.droppedEvents += 1;
|
|
3819
3854
|
this.diagnostic("invalid-event-timestamp");
|
|
@@ -3842,7 +3877,7 @@ var MobileReplayRecorder = class {
|
|
|
3842
3877
|
}
|
|
3843
3878
|
}
|
|
3844
3879
|
async closeCurrent(isFinal) {
|
|
3845
|
-
const closesFinalChunkSlot = !isFinal && !this.rotatingSession && this.uploadActive && this.sessionStore?.getNextChunkIndex() === MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1;
|
|
3880
|
+
const closesFinalChunkSlot = !isFinal && !this.rotatingSession && !this.sealedForIdle && this.uploadActive && this.sessionStore?.getNextChunkIndex() === MAX_SESSION_REPLAY_CHUNKS_PER_SESSION - 1;
|
|
3846
3881
|
if (closesFinalChunkSlot) {
|
|
3847
3882
|
this.rotatingSession = true;
|
|
3848
3883
|
const rotationOptions = {};
|
|
@@ -3854,7 +3889,7 @@ var MobileReplayRecorder = class {
|
|
|
3854
3889
|
}
|
|
3855
3890
|
this.chunkBuffer.append(
|
|
3856
3891
|
createCustomEvent(
|
|
3857
|
-
|
|
3892
|
+
SESSION_ROTATED_CUSTOM_EVENT_TAG,
|
|
3858
3893
|
{ reason: "chunk-cap" },
|
|
3859
3894
|
this.now()
|
|
3860
3895
|
),
|
|
@@ -4054,15 +4089,19 @@ var MobileReplayRecorder = class {
|
|
|
4054
4089
|
if (!policyReady) {
|
|
4055
4090
|
return;
|
|
4056
4091
|
}
|
|
4057
|
-
|
|
4092
|
+
const sessionIdBeforeResume = this.identity?.sessionId;
|
|
4093
|
+
await this.maybeRotateSession(this.now(), true);
|
|
4058
4094
|
if (!this.running || !this.isCurrentGeneration(generation)) {
|
|
4059
4095
|
return;
|
|
4060
4096
|
}
|
|
4061
4097
|
this.appendCustom(VISIBILITY_CUSTOM_EVENT_TAG, { state: "visible" });
|
|
4062
|
-
this.
|
|
4098
|
+
const openedOnSnapshot = this.identity?.sessionId !== sessionIdBeforeResume && this.lastFullSnapshotAtUnixMs > 0;
|
|
4099
|
+
if (!openedOnSnapshot) {
|
|
4100
|
+
this.forceFullSnapshot = true;
|
|
4101
|
+
}
|
|
4063
4102
|
this.startTimers();
|
|
4064
4103
|
this.diagnosticsStatus = this.consentState() === "Unknown" ? "consent-required" : "recording";
|
|
4065
|
-
await this.captureTick(
|
|
4104
|
+
await this.captureTick(!openedOnSnapshot);
|
|
4066
4105
|
if (this.uploadActive && this.running) {
|
|
4067
4106
|
this.transport?.resume();
|
|
4068
4107
|
}
|
|
@@ -4342,9 +4381,12 @@ var MobileReplayRecorder = class {
|
|
|
4342
4381
|
if (!this.running || identityEpoch !== this.identityCancellationEpoch) {
|
|
4343
4382
|
return;
|
|
4344
4383
|
}
|
|
4384
|
+
if (!this.rotationPromise && !this.sealedForIdle && this.rotationReason(this.now()) === "idle") {
|
|
4385
|
+
await this.maybeRotateSession(this.now());
|
|
4386
|
+
}
|
|
4345
4387
|
if (this.rotationPromise) {
|
|
4346
4388
|
await this.rotationPromise;
|
|
4347
|
-
} else {
|
|
4389
|
+
} else if (!this.sealedForIdle) {
|
|
4348
4390
|
this.rotatingSession = true;
|
|
4349
4391
|
const rotation = this.performSessionRotation(
|
|
4350
4392
|
this.now(),
|
|
@@ -4679,16 +4721,24 @@ var MobileReplayRecorder = class {
|
|
|
4679
4721
|
chunkIndex: 0
|
|
4680
4722
|
};
|
|
4681
4723
|
}
|
|
4724
|
+
/*
|
|
4725
|
+
* Idle before the duration cap, the order Common's SessionIdentity decides
|
|
4726
|
+
* them in. A session left for the idle window ended where it was left,
|
|
4727
|
+
* even when it has outlived the cap by the time anything notices, and
|
|
4728
|
+
* only an idle rotation dates its end there (see performSessionRotation):
|
|
4729
|
+
* called "duration", a session three hours old put in the background for
|
|
4730
|
+
* two more was sealed two hours after its footage.
|
|
4731
|
+
*/
|
|
4682
4732
|
rotationReason(nowUnixMs) {
|
|
4683
4733
|
if (!this.identity) {
|
|
4684
4734
|
return null;
|
|
4685
4735
|
}
|
|
4686
|
-
if (nowUnixMs - this.identity.sessionStartUnixMs >= SESSION_REPLAY_MAX_SESSION_MS) {
|
|
4687
|
-
return "duration";
|
|
4688
|
-
}
|
|
4689
4736
|
if (nowUnixMs - this.sessionLastActivityUnixMs >= SESSION_REPLAY_IDLE_ROLLOVER_MS) {
|
|
4690
4737
|
return "idle";
|
|
4691
4738
|
}
|
|
4739
|
+
if (nowUnixMs - this.identity.sessionStartUnixMs >= SESSION_REPLAY_MAX_SESSION_MS) {
|
|
4740
|
+
return "duration";
|
|
4741
|
+
}
|
|
4692
4742
|
return null;
|
|
4693
4743
|
}
|
|
4694
4744
|
runExternalActivity(nowUnixMs, action) {
|
|
@@ -4723,23 +4773,37 @@ var MobileReplayRecorder = class {
|
|
|
4723
4773
|
}
|
|
4724
4774
|
});
|
|
4725
4775
|
}
|
|
4726
|
-
|
|
4727
|
-
|
|
4776
|
+
/*
|
|
4777
|
+
* isUserBack: the caller knows the user is here - a touch, the app
|
|
4778
|
+
* returning to the foreground, an explicit captureSession(). An idle
|
|
4779
|
+
* session then rotates at once. Anything else (a capture tick, a route
|
|
4780
|
+
* change, a developer event) only finds out the session went idle, and
|
|
4781
|
+
* seals it to wait for them (see sealedForIdle).
|
|
4782
|
+
*/
|
|
4783
|
+
async maybeRotateSession(nowUnixMs, isUserBack = false) {
|
|
4784
|
+
while (this.rotationPromise) {
|
|
4728
4785
|
await this.rotationPromise;
|
|
4729
|
-
|
|
4786
|
+
if (!isUserBack || !this.sealedForIdle) {
|
|
4787
|
+
return;
|
|
4788
|
+
}
|
|
4730
4789
|
}
|
|
4731
4790
|
if (!this.running || this.rotatingSession || !this.identity || !this.config || !this.sessionStore || !this.options) {
|
|
4732
4791
|
return;
|
|
4733
4792
|
}
|
|
4734
|
-
|
|
4793
|
+
let reason;
|
|
4794
|
+
if (this.sealedForIdle) {
|
|
4795
|
+
if (!isUserBack) {
|
|
4796
|
+
return;
|
|
4797
|
+
}
|
|
4798
|
+
reason = "idle";
|
|
4799
|
+
} else {
|
|
4800
|
+
reason = this.rotationReason(nowUnixMs);
|
|
4801
|
+
}
|
|
4735
4802
|
if (!reason) {
|
|
4736
4803
|
return;
|
|
4737
4804
|
}
|
|
4738
4805
|
this.rotatingSession = true;
|
|
4739
|
-
const operation = this.performSessionRotation(
|
|
4740
|
-
nowUnixMs,
|
|
4741
|
-
reason
|
|
4742
|
-
);
|
|
4806
|
+
const operation = reason === "idle" && !isUserBack ? this.sealForIdle() : this.performSessionRotation(nowUnixMs, reason);
|
|
4743
4807
|
this.rotationPromise = operation;
|
|
4744
4808
|
try {
|
|
4745
4809
|
await operation;
|
|
@@ -4750,8 +4814,60 @@ var MobileReplayRecorder = class {
|
|
|
4750
4814
|
this.rotatingSession = false;
|
|
4751
4815
|
}
|
|
4752
4816
|
}
|
|
4817
|
+
/*
|
|
4818
|
+
* maybeRotateSession for a user who is here. On an app whose session
|
|
4819
|
+
* ended idle, the policy is brought up to date first: no capture tick has
|
|
4820
|
+
* refreshed it while the app sat idle, and the next session must not open
|
|
4821
|
+
* under one that has since changed (or turned replay off).
|
|
4822
|
+
*/
|
|
4823
|
+
async maybeRotateForUser(nowUnixMs) {
|
|
4824
|
+
if (this.sealedForIdle && !this.rotatingSession) {
|
|
4825
|
+
const policyReady = await this.refreshRuntimePolicy(
|
|
4826
|
+
this.lifecycleGeneration,
|
|
4827
|
+
false,
|
|
4828
|
+
"periodic"
|
|
4829
|
+
);
|
|
4830
|
+
if (!policyReady) {
|
|
4831
|
+
return;
|
|
4832
|
+
}
|
|
4833
|
+
}
|
|
4834
|
+
await this.maybeRotateSession(nowUnixMs, true);
|
|
4835
|
+
}
|
|
4836
|
+
/*
|
|
4837
|
+
* End the session as idle, where it really ended, and wait for the user.
|
|
4838
|
+
*
|
|
4839
|
+
* Sealed with whatever footage is still buffered - dated by that footage,
|
|
4840
|
+
* never by now - and with no marker of its own: a marker stamped now
|
|
4841
|
+
* would date the session's end half an hour or more after anything the
|
|
4842
|
+
* player can show. With nothing buffered nothing is sent, and the server
|
|
4843
|
+
* ends the session at its last chunk.
|
|
4844
|
+
*/
|
|
4845
|
+
async sealForIdle() {
|
|
4846
|
+
this.sealedForIdle = true;
|
|
4847
|
+
this.diagnostic("session-ended-idle", {
|
|
4848
|
+
idleForMs: Math.max(0, this.now() - this.sessionLastActivityUnixMs)
|
|
4849
|
+
});
|
|
4850
|
+
this.notifySessionChange();
|
|
4851
|
+
await this.closeCurrent(true);
|
|
4852
|
+
await this.closeQueue;
|
|
4853
|
+
if (!this.running) {
|
|
4854
|
+
return;
|
|
4855
|
+
}
|
|
4856
|
+
if (this.uploadActive) {
|
|
4857
|
+
await this.transport?.drain();
|
|
4858
|
+
} else {
|
|
4859
|
+
this.rollingBuffer.clear();
|
|
4860
|
+
}
|
|
4861
|
+
}
|
|
4753
4862
|
async performSessionRotation(nowUnixMs, reason) {
|
|
4754
|
-
|
|
4863
|
+
if (reason !== "idle") {
|
|
4864
|
+
this.appendCustom(
|
|
4865
|
+
SESSION_ROTATED_CUSTOM_EVENT_TAG,
|
|
4866
|
+
{ reason },
|
|
4867
|
+
void 0,
|
|
4868
|
+
true
|
|
4869
|
+
);
|
|
4870
|
+
}
|
|
4755
4871
|
await this.closeCurrent(true);
|
|
4756
4872
|
await this.closeQueue;
|
|
4757
4873
|
if (!this.running) {
|
|
@@ -4766,12 +4882,64 @@ var MobileReplayRecorder = class {
|
|
|
4766
4882
|
return;
|
|
4767
4883
|
}
|
|
4768
4884
|
await this.completeSessionIdentityRotation(nowUnixMs, reason);
|
|
4885
|
+
if (reason === "idle" && this.running) {
|
|
4886
|
+
await this.openSessionAfterIdle();
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
/*
|
|
4890
|
+
* The first things a session after an idle one records: a full snapshot
|
|
4891
|
+
* of the screen the user came back to, then the rotation marker - the
|
|
4892
|
+
* snapshot first, so chunk 0 is a seek anchor rather than a lone marker.
|
|
4893
|
+
*
|
|
4894
|
+
* Taken here, inside the rotation, rather than left to the next capture
|
|
4895
|
+
* tick: until the rotation settles, touches are dropped and every other
|
|
4896
|
+
* event waits for it (rotatingSession), so nothing can land ahead of the
|
|
4897
|
+
* snapshot. A user who has just come back is touching the screen, and
|
|
4898
|
+
* half a second of that was otherwise a chunk 0 with no screen in it.
|
|
4899
|
+
*
|
|
4900
|
+
* In the background or without a registered root there is no screen to
|
|
4901
|
+
* capture yet; the marker opens the session alone.
|
|
4902
|
+
*/
|
|
4903
|
+
async openSessionAfterIdle() {
|
|
4904
|
+
const generation = this.lifecycleGeneration;
|
|
4905
|
+
const rootTag = this.rootTag;
|
|
4906
|
+
const options = this.options;
|
|
4907
|
+
const sessionId = this.identity?.sessionId;
|
|
4908
|
+
if (this.foreground && rootTag && options && sessionId) {
|
|
4909
|
+
const capturedAtUnixMs = this.now();
|
|
4910
|
+
let tree;
|
|
4911
|
+
try {
|
|
4912
|
+
tree = await this.nativeViewTree.captureViewTree(rootTag);
|
|
4913
|
+
} catch (error) {
|
|
4914
|
+
if (this.running && this.isCurrentGeneration(generation)) {
|
|
4915
|
+
this.disable("native-capture-failed", {
|
|
4916
|
+
name: error instanceof Error ? error.name : "Error"
|
|
4917
|
+
});
|
|
4918
|
+
this.haltCapture();
|
|
4919
|
+
}
|
|
4920
|
+
return;
|
|
4921
|
+
}
|
|
4922
|
+
if (!this.running || !this.isCurrentGeneration(generation) || this.identity?.sessionId !== sessionId) {
|
|
4923
|
+
return;
|
|
4924
|
+
}
|
|
4925
|
+
if (this.foreground) {
|
|
4926
|
+
this.touchPrivacyMap = deriveTouchPrivacyMap(tree);
|
|
4927
|
+
this.appendCapture(tree, options, capturedAtUnixMs, true);
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
this.appendCustom(
|
|
4931
|
+
SESSION_ROTATED_CUSTOM_EVENT_TAG,
|
|
4932
|
+
{ reason: "idle" },
|
|
4933
|
+
void 0,
|
|
4934
|
+
true
|
|
4935
|
+
);
|
|
4769
4936
|
}
|
|
4770
4937
|
async completeSessionIdentityRotation(nowUnixMs, reason) {
|
|
4771
4938
|
if (!this.sessionStore || !this.config || !this.options) {
|
|
4772
4939
|
return;
|
|
4773
4940
|
}
|
|
4774
4941
|
this.identity = this.identityPersisted ? await this.sessionStore.rotate(nowUnixMs) : this.ephemeralIdentity(nowUnixMs);
|
|
4942
|
+
this.sealedForIdle = false;
|
|
4775
4943
|
this.notifySessionChange();
|
|
4776
4944
|
this.sampled = this.identityPersisted ? deterministicSample(
|
|
4777
4945
|
this.identity.sessionId,
|