@anyreplay/browser 0.2.3 → 0.3.0
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 +13 -9
- package/dist/index.cjs +58 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +24 -12
- package/dist/index.d.ts +24 -12
- package/dist/index.js +58 -16
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.cts
CHANGED
|
@@ -50,6 +50,12 @@ interface TransportHooks {
|
|
|
50
50
|
onResyncNeeded?: () => void;
|
|
51
51
|
/** Debug-only explanations: an event dropped, a body limit lowered. */
|
|
52
52
|
onWarning?: (message: string) => void;
|
|
53
|
+
/**
|
|
54
|
+
* What the project stored for masking typed text, from an accepted chunk's
|
|
55
|
+
* answer (`maskAllInputs` in the 202). Called on every answer that carries
|
|
56
|
+
* it; an ingest from before this field never calls it.
|
|
57
|
+
*/
|
|
58
|
+
onProjectMasking?: (maskAllInputs: boolean) => void;
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Ships buffered events to ingest.
|
|
@@ -340,19 +346,25 @@ interface AnyReplayOptions {
|
|
|
340
346
|
/**
|
|
341
347
|
* Mask the value of every form control.
|
|
342
348
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
+
* On by default: what a visitor types into an input or a textarea reaches a
|
|
350
|
+
* recording as asterisks, so a replay shows a field being filled in without
|
|
351
|
+
* showing what went into it. Set `maskAllInputs: false` to record typed
|
|
352
|
+
* values instead — for a site whose forms are worth reading in a replay —
|
|
353
|
+
* and mark the fields that should stay hidden with the `maskClass`.
|
|
354
|
+
*
|
|
355
|
+
* Some fields are masked whatever this says, in every mode, `false`
|
|
356
|
+
* included: passwords, one-time codes, payment fields (card number, CVV,
|
|
357
|
+
* expiry, IBAN, routing number, sort code), anything inside an element with
|
|
358
|
+
* the mask class, and any value that looks like a card number. Email and
|
|
359
|
+
* telephone fields are not on that list — they are ordinary fields, masked
|
|
360
|
+
* by default and recorded when this is `false`. See `isSensitiveField` in
|
|
361
|
+
* `masking.ts` for the exact list.
|
|
349
362
|
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* option. See `isSensitiveField` in `masking.ts` for the exact list.
|
|
363
|
+
* Left out, the project decides: the page records masked until ingest
|
|
364
|
+
* answers the first chunk with the project's stored choice (the setup
|
|
365
|
+
* page's switch), and records typed values from then on if the project
|
|
366
|
+
* chose to. Set either value here and the project's choice is not asked
|
|
367
|
+
* for. See `applyProjectMasking` in `recorder.ts` and contract §6.2.
|
|
356
368
|
*/
|
|
357
369
|
maskAllInputs?: boolean;
|
|
358
370
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,12 @@ interface TransportHooks {
|
|
|
50
50
|
onResyncNeeded?: () => void;
|
|
51
51
|
/** Debug-only explanations: an event dropped, a body limit lowered. */
|
|
52
52
|
onWarning?: (message: string) => void;
|
|
53
|
+
/**
|
|
54
|
+
* What the project stored for masking typed text, from an accepted chunk's
|
|
55
|
+
* answer (`maskAllInputs` in the 202). Called on every answer that carries
|
|
56
|
+
* it; an ingest from before this field never calls it.
|
|
57
|
+
*/
|
|
58
|
+
onProjectMasking?: (maskAllInputs: boolean) => void;
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Ships buffered events to ingest.
|
|
@@ -340,19 +346,25 @@ interface AnyReplayOptions {
|
|
|
340
346
|
/**
|
|
341
347
|
* Mask the value of every form control.
|
|
342
348
|
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
*
|
|
349
|
+
* On by default: what a visitor types into an input or a textarea reaches a
|
|
350
|
+
* recording as asterisks, so a replay shows a field being filled in without
|
|
351
|
+
* showing what went into it. Set `maskAllInputs: false` to record typed
|
|
352
|
+
* values instead — for a site whose forms are worth reading in a replay —
|
|
353
|
+
* and mark the fields that should stay hidden with the `maskClass`.
|
|
354
|
+
*
|
|
355
|
+
* Some fields are masked whatever this says, in every mode, `false`
|
|
356
|
+
* included: passwords, one-time codes, payment fields (card number, CVV,
|
|
357
|
+
* expiry, IBAN, routing number, sort code), anything inside an element with
|
|
358
|
+
* the mask class, and any value that looks like a card number. Email and
|
|
359
|
+
* telephone fields are not on that list — they are ordinary fields, masked
|
|
360
|
+
* by default and recorded when this is `false`. See `isSensitiveField` in
|
|
361
|
+
* `masking.ts` for the exact list.
|
|
349
362
|
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
*
|
|
355
|
-
* option. See `isSensitiveField` in `masking.ts` for the exact list.
|
|
363
|
+
* Left out, the project decides: the page records masked until ingest
|
|
364
|
+
* answers the first chunk with the project's stored choice (the setup
|
|
365
|
+
* page's switch), and records typed values from then on if the project
|
|
366
|
+
* chose to. Set either value here and the project's choice is not asked
|
|
367
|
+
* for. See `applyProjectMasking` in `recorder.ts` and contract §6.2.
|
|
356
368
|
*/
|
|
357
369
|
maskAllInputs?: boolean;
|
|
358
370
|
/**
|
package/dist/index.js
CHANGED
|
@@ -3960,11 +3960,12 @@ function sha256HexSync(bytes) {
|
|
|
3960
3960
|
var DEFAULTS = {
|
|
3961
3961
|
ingestUrl: "https://in.anyreplay.com",
|
|
3962
3962
|
sampleRate: 1,
|
|
3963
|
-
//
|
|
3964
|
-
//
|
|
3965
|
-
//
|
|
3966
|
-
//
|
|
3967
|
-
|
|
3963
|
+
// Form fields are masked unless the page opts out with `maskAllInputs:
|
|
3964
|
+
// false`. Editable regions are opt-in, because masking one hides all of its
|
|
3965
|
+
// text, not only what was typed. Neither option reaches the floor in
|
|
3966
|
+
// masking.ts — passwords, payment fields and the rest — which no option can
|
|
3967
|
+
// switch off.
|
|
3968
|
+
maskAllInputs: true,
|
|
3968
3969
|
maskAllTyping: false,
|
|
3969
3970
|
maskClass: "ar-mask",
|
|
3970
3971
|
blockClass: "ar-block",
|
|
@@ -4030,7 +4031,7 @@ function readSwitch(input, name) {
|
|
|
4030
4031
|
const value = input[name];
|
|
4031
4032
|
if (typeof value === "boolean") return value;
|
|
4032
4033
|
if (value !== void 0 && typeof console !== "undefined") {
|
|
4033
|
-
const consequence = name.
|
|
4034
|
+
const consequence = name === "maskAllInputs" ? "Form fields stay masked, as by default." : name === "maskAllTyping" ? "Nothing more is masked by this option." : `${name.slice("record".length)} are recorded, as by default.`;
|
|
4034
4035
|
console.warn(`[anyreplay] ${name} ignored: expected true or false, got ${typeof value}. ${consequence}`);
|
|
4035
4036
|
}
|
|
4036
4037
|
return DEFAULTS[name];
|
|
@@ -4587,17 +4588,20 @@ var BACKOFF_BASE_MS = 2e3;
|
|
|
4587
4588
|
var BACKOFF_MAX_MS = 6e4;
|
|
4588
4589
|
var MAX_RETRY_AFTER_MS = 10 * 6e4;
|
|
4589
4590
|
var MAX_CONSECUTIVE_REFUSALS = 3;
|
|
4590
|
-
async function
|
|
4591
|
+
async function readReply(response) {
|
|
4591
4592
|
try {
|
|
4592
4593
|
if (typeof response.json !== "function") return void 0;
|
|
4593
4594
|
const reply = await response.json();
|
|
4594
|
-
|
|
4595
|
-
const next = reply.nextSeq;
|
|
4596
|
-
return typeof next === "number" && Number.isInteger(next) && next > sent ? next : void 0;
|
|
4595
|
+
return reply && typeof reply === "object" ? reply : void 0;
|
|
4597
4596
|
} catch (e) {
|
|
4598
4597
|
return void 0;
|
|
4599
4598
|
}
|
|
4600
4599
|
}
|
|
4600
|
+
function staleSeqHint(reply, sent) {
|
|
4601
|
+
if ((reply == null ? void 0 : reply.duplicate) !== true) return void 0;
|
|
4602
|
+
const next = reply.nextSeq;
|
|
4603
|
+
return typeof next === "number" && Number.isInteger(next) && next > sent ? next : void 0;
|
|
4604
|
+
}
|
|
4601
4605
|
function retryAfterMs(response, now = Date.now()) {
|
|
4602
4606
|
var _a, _b;
|
|
4603
4607
|
let raw;
|
|
@@ -4849,7 +4853,7 @@ var Transport = class {
|
|
|
4849
4853
|
}
|
|
4850
4854
|
/** One request, and what to do next. */
|
|
4851
4855
|
async send(chunk) {
|
|
4852
|
-
var _a, _b, _c, _d, _e, _f;
|
|
4856
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
4853
4857
|
const retry = chunk.attempts > 0;
|
|
4854
4858
|
chunk.attempts += 1;
|
|
4855
4859
|
(_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, chunk.seq + 1);
|
|
@@ -4870,7 +4874,14 @@ var Transport = class {
|
|
|
4870
4874
|
}
|
|
4871
4875
|
if (this.state === "stopped" || this.pending !== chunk) return "stopped";
|
|
4872
4876
|
if (response.ok) {
|
|
4873
|
-
const
|
|
4877
|
+
const reply = await readReply(response);
|
|
4878
|
+
if (typeof (reply == null ? void 0 : reply.maskAllInputs) === "boolean") {
|
|
4879
|
+
try {
|
|
4880
|
+
(_d = (_c = this.hooks).onProjectMasking) == null ? void 0 : _d.call(_c, reply.maskAllInputs);
|
|
4881
|
+
} catch (e) {
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
const nextSeq = retry ? void 0 : staleSeqHint(reply, chunk.seq);
|
|
4874
4885
|
if (nextSeq !== void 0 && this.staleRecoveries < MAX_STALE_RECOVERIES) {
|
|
4875
4886
|
this.staleRecoveries += 1;
|
|
4876
4887
|
chunk.seq = nextSeq;
|
|
@@ -4884,7 +4895,7 @@ var Transport = class {
|
|
|
4884
4895
|
this.consecutiveFailures = 0;
|
|
4885
4896
|
this.consecutiveRefusals = 0;
|
|
4886
4897
|
this.backoffUntil = 0;
|
|
4887
|
-
(
|
|
4898
|
+
(_f = (_e = this.hooks).onSent) == null ? void 0 : _f.call(_e, chunk.seq, chunk.events.length);
|
|
4888
4899
|
return "sent";
|
|
4889
4900
|
}
|
|
4890
4901
|
const status = response.status;
|
|
@@ -4896,7 +4907,7 @@ var Transport = class {
|
|
|
4896
4907
|
this.stop(`rejected_${status}`);
|
|
4897
4908
|
return "stopped";
|
|
4898
4909
|
}
|
|
4899
|
-
(
|
|
4910
|
+
(_h = (_g = this.hooks).onWarning) == null ? void 0 : _h.call(_g, `ingest refused a chunk (${status}); its ${chunk.events.length} events are dropped`);
|
|
4900
4911
|
this.afterGap();
|
|
4901
4912
|
return "again";
|
|
4902
4913
|
}
|
|
@@ -5071,7 +5082,7 @@ var Transport = class {
|
|
|
5071
5082
|
|
|
5072
5083
|
// src/version.ts
|
|
5073
5084
|
var SDK_NAME = "@anyreplay/browser";
|
|
5074
|
-
var SDK_VERSION = true ? "0.
|
|
5085
|
+
var SDK_VERSION = true ? "0.3.0" : "0.0.0-dev";
|
|
5075
5086
|
var PLATFORM = "web";
|
|
5076
5087
|
|
|
5077
5088
|
// src/visitor.ts
|
|
@@ -5180,6 +5191,7 @@ function forgetVisitor(local, session) {
|
|
|
5180
5191
|
// src/recorder.ts
|
|
5181
5192
|
var RRWEB_DOM_CONTENT_LOADED = 0;
|
|
5182
5193
|
var RRWEB_LOAD = 1;
|
|
5194
|
+
var EVENT_FULL_SNAPSHOT3 = 2;
|
|
5183
5195
|
var RAGE_CLICK_COUNT = 3;
|
|
5184
5196
|
var RAGE_CLICK_WINDOW_MS = 1e3;
|
|
5185
5197
|
var RAGE_CLICK_RADIUS_PX = 30;
|
|
@@ -5187,6 +5199,7 @@ var MAX_PENDING_EVENTS = 32;
|
|
|
5187
5199
|
var EDITABLE_SELECTOR = '[contenteditable]:not([contenteditable="false"])';
|
|
5188
5200
|
function createRecorder(rawOptions, shell) {
|
|
5189
5201
|
const options = resolveOptions(rawOptions);
|
|
5202
|
+
let awaitingProjectMasking = typeof rawOptions.maskAllInputs !== "boolean" && !options.maskAllTyping;
|
|
5190
5203
|
const local = safeStorage(() => globalThis.localStorage);
|
|
5191
5204
|
const session = safeStorage(() => globalThis.sessionStorage);
|
|
5192
5205
|
let status = "idle";
|
|
@@ -5196,6 +5209,8 @@ function createRecorder(rawOptions, shell) {
|
|
|
5196
5209
|
let diagnostics;
|
|
5197
5210
|
let assets;
|
|
5198
5211
|
let pageCount = 0;
|
|
5212
|
+
let applyProjectMasking = () => {
|
|
5213
|
+
};
|
|
5199
5214
|
let pendingTraits;
|
|
5200
5215
|
let traitsAwaitingSession;
|
|
5201
5216
|
let traitsToRetry;
|
|
@@ -5369,6 +5384,9 @@ function createRecorder(rawOptions, shell) {
|
|
|
5369
5384
|
onWarning: (message) => {
|
|
5370
5385
|
if (options.debug) console.warn(`[anyreplay] ${message}`);
|
|
5371
5386
|
},
|
|
5387
|
+
// Out of the transport's own call stack, like a resync: restarting
|
|
5388
|
+
// rrweb emits a snapshot into the channel that is answering.
|
|
5389
|
+
onProjectMasking: (masked) => setTimeout(() => applyProjectMasking(channel, masked), 0),
|
|
5372
5390
|
// Events were dropped (a long outage, or one too large to send), so
|
|
5373
5391
|
// the DOM changes after the gap describe nodes ingest never got. A
|
|
5374
5392
|
// fresh full snapshot starts the page over. Taken on the next turn,
|
|
@@ -5409,7 +5427,8 @@ function createRecorder(rawOptions, shell) {
|
|
|
5409
5427
|
maskEveryField: options.maskAllInputs || options.maskAllTyping,
|
|
5410
5428
|
maskClass: options.maskClass
|
|
5411
5429
|
};
|
|
5412
|
-
|
|
5430
|
+
let flushedEarly = false;
|
|
5431
|
+
const capture = () => record({
|
|
5413
5432
|
emit: (event) => {
|
|
5414
5433
|
const typed = event;
|
|
5415
5434
|
if (typed.type === RRWEB_DOM_CONTENT_LOADED || typed.type === RRWEB_LOAD) return;
|
|
@@ -5419,6 +5438,12 @@ function createRecorder(rawOptions, shell) {
|
|
|
5419
5438
|
touchSession(session, typed.timestamp);
|
|
5420
5439
|
channel.push(typed);
|
|
5421
5440
|
drainPending();
|
|
5441
|
+
if (awaitingProjectMasking && !flushedEarly && typed.type === EVENT_FULL_SNAPSHOT3) {
|
|
5442
|
+
flushedEarly = true;
|
|
5443
|
+
setTimeout(() => {
|
|
5444
|
+
void channel.flush();
|
|
5445
|
+
}, 0);
|
|
5446
|
+
}
|
|
5422
5447
|
},
|
|
5423
5448
|
maskInputOptions: policy.maskEveryField ? MASK_EVERY_FIELD : MASK_SENSITIVE_FIELDS,
|
|
5424
5449
|
maskTextClass: options.maskClass,
|
|
@@ -5431,6 +5456,23 @@ function createRecorder(rawOptions, shell) {
|
|
|
5431
5456
|
recordCanvas: false,
|
|
5432
5457
|
collectFonts: false
|
|
5433
5458
|
});
|
|
5459
|
+
applyProjectMasking = (from, masked) => {
|
|
5460
|
+
if (!awaitingProjectMasking || from !== transport) return;
|
|
5461
|
+
awaitingProjectMasking = false;
|
|
5462
|
+
if (masked || status !== "recording" || !stopRecording) return;
|
|
5463
|
+
policy.maskEveryField = false;
|
|
5464
|
+
try {
|
|
5465
|
+
stopRecording();
|
|
5466
|
+
stopRecording = capture();
|
|
5467
|
+
if (!stopRecording) throw new Error("rrweb did not start");
|
|
5468
|
+
} catch (e) {
|
|
5469
|
+
stopRecording = void 0;
|
|
5470
|
+
from.stop("restart_failed");
|
|
5471
|
+
return;
|
|
5472
|
+
}
|
|
5473
|
+
if (options.debug) console.warn("[anyreplay] recording typed text: the project chose to (maskAllInputs: false)");
|
|
5474
|
+
};
|
|
5475
|
+
stopRecording = capture();
|
|
5434
5476
|
drainPending();
|
|
5435
5477
|
if (pendingTraits) {
|
|
5436
5478
|
sendIdentity(who, pendingTraits);
|