@anyreplay/browser 0.1.4 → 0.2.1
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 +20 -2
- package/dist/index.cjs +1188 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +599 -97
- package/dist/index.d.ts +599 -97
- package/dist/index.js +1172 -135
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -3637,6 +3637,325 @@ record.takeFullSnapshot = (isCheckout) => {
|
|
|
3637
3637
|
};
|
|
3638
3638
|
record.mirror = mirror;
|
|
3639
3639
|
|
|
3640
|
+
// src/assets.ts
|
|
3641
|
+
var ASSET_TAG = "anyreplay.asset";
|
|
3642
|
+
var ASSET_LIMITS = {
|
|
3643
|
+
maxImageBytes: 3 * 1024 * 1024,
|
|
3644
|
+
maxFontBytes: 160 * 1024,
|
|
3645
|
+
maxSvgBytes: 48 * 1024,
|
|
3646
|
+
inlineBudgetBytes: 1024 * 1024,
|
|
3647
|
+
maxUrls: 300
|
|
3648
|
+
};
|
|
3649
|
+
function defaultAssetMode(loc = currentLocation()) {
|
|
3650
|
+
if (!loc) return "reference";
|
|
3651
|
+
const protocol = loc.protocol.toLowerCase();
|
|
3652
|
+
return protocol === "http:" || protocol === "https:" ? "reference" : "inline";
|
|
3653
|
+
}
|
|
3654
|
+
function localAssetPrefix(loc = currentLocation()) {
|
|
3655
|
+
if (!loc) return null;
|
|
3656
|
+
const protocol = loc.protocol.toLowerCase();
|
|
3657
|
+
if (protocol === "file:") {
|
|
3658
|
+
const folder = loc.pathname.slice(0, loc.pathname.lastIndexOf("/") + 1);
|
|
3659
|
+
return folder ? `file://${folder}` : null;
|
|
3660
|
+
}
|
|
3661
|
+
if (!loc.host) return null;
|
|
3662
|
+
return `${protocol}//${loc.host}/`;
|
|
3663
|
+
}
|
|
3664
|
+
function currentLocation() {
|
|
3665
|
+
try {
|
|
3666
|
+
return typeof location !== "undefined" ? location : void 0;
|
|
3667
|
+
} catch (e) {
|
|
3668
|
+
return void 0;
|
|
3669
|
+
}
|
|
3670
|
+
}
|
|
3671
|
+
function classify(url) {
|
|
3672
|
+
const path = url.split(/[?#]/, 1)[0].toLowerCase();
|
|
3673
|
+
const ext = path.slice(path.lastIndexOf(".") + 1);
|
|
3674
|
+
switch (ext) {
|
|
3675
|
+
case "png":
|
|
3676
|
+
case "jpg":
|
|
3677
|
+
case "jpeg":
|
|
3678
|
+
case "gif":
|
|
3679
|
+
case "webp":
|
|
3680
|
+
return { kind: "image" };
|
|
3681
|
+
case "svg":
|
|
3682
|
+
return { kind: "inline", mime: "image/svg+xml", cap: "maxSvgBytes" };
|
|
3683
|
+
case "woff2":
|
|
3684
|
+
return { kind: "inline", mime: "font/woff2", cap: "maxFontBytes" };
|
|
3685
|
+
case "woff":
|
|
3686
|
+
return { kind: "inline", mime: "font/woff", cap: "maxFontBytes" };
|
|
3687
|
+
case "ttf":
|
|
3688
|
+
return { kind: "inline", mime: "font/ttf", cap: "maxFontBytes" };
|
|
3689
|
+
case "otf":
|
|
3690
|
+
return { kind: "inline", mime: "font/otf", cap: "maxFontBytes" };
|
|
3691
|
+
default:
|
|
3692
|
+
return null;
|
|
3693
|
+
}
|
|
3694
|
+
}
|
|
3695
|
+
var DEVICE_FILE_PATH = /^(_capacitor_|__cdvfile_)/;
|
|
3696
|
+
var EVENT_FULL_SNAPSHOT = 2;
|
|
3697
|
+
var EVENT_INCREMENTAL = 3;
|
|
3698
|
+
var SOURCES_WITH_URLS = /* @__PURE__ */ new Set([
|
|
3699
|
+
0,
|
|
3700
|
+
// Mutation: added nodes, changed attributes (`src`, `style`, `srcset`)
|
|
3701
|
+
8,
|
|
3702
|
+
// StyleSheetRule: a rule inserted with insertRule
|
|
3703
|
+
13,
|
|
3704
|
+
// StyleDeclaration: a property set through CSSOM
|
|
3705
|
+
15
|
|
3706
|
+
// AdoptedStyleSheet: constructed stylesheets (Ionic's web components)
|
|
3707
|
+
]);
|
|
3708
|
+
var escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3709
|
+
var AssetCollector = class {
|
|
3710
|
+
constructor(deps) {
|
|
3711
|
+
this.deps = deps;
|
|
3712
|
+
__publicField(this, "limits");
|
|
3713
|
+
__publicField(this, "pattern");
|
|
3714
|
+
__publicField(this, "seen", /* @__PURE__ */ new Set());
|
|
3715
|
+
__publicField(this, "queue", []);
|
|
3716
|
+
/** Hashes ingest confirmed this page, so two URLs with the same bytes ask once. */
|
|
3717
|
+
__publicField(this, "confirmed", /* @__PURE__ */ new Set());
|
|
3718
|
+
__publicField(this, "inlineSpent", 0);
|
|
3719
|
+
__publicField(this, "running", false);
|
|
3720
|
+
__publicField(this, "stopped", false);
|
|
3721
|
+
this.limits = { ...ASSET_LIMITS, ...deps.limits };
|
|
3722
|
+
this.pattern = new RegExp(`${escapeRegExp(deps.prefix)}[^\\s"'()<>,\\\\]+`, "g");
|
|
3723
|
+
}
|
|
3724
|
+
/** Notes every URL of the app's files in one event. Cheap when there are none. */
|
|
3725
|
+
scan(event) {
|
|
3726
|
+
var _a;
|
|
3727
|
+
if (this.stopped) return;
|
|
3728
|
+
if (event.type === EVENT_FULL_SNAPSHOT) {
|
|
3729
|
+
this.walk(event.data, 0);
|
|
3730
|
+
} else if (event.type === EVENT_INCREMENTAL) {
|
|
3731
|
+
const source = (_a = event.data) == null ? void 0 : _a.source;
|
|
3732
|
+
if (source !== void 0 && SOURCES_WITH_URLS.has(source)) this.walk(event.data, 0);
|
|
3733
|
+
}
|
|
3734
|
+
}
|
|
3735
|
+
stop() {
|
|
3736
|
+
this.stopped = true;
|
|
3737
|
+
this.queue.length = 0;
|
|
3738
|
+
}
|
|
3739
|
+
/** Resolves once everything queued so far has been handled. For tests. */
|
|
3740
|
+
async idle() {
|
|
3741
|
+
while (this.running || this.queue.length > 0) await new Promise((resolve) => setTimeout(resolve, 1));
|
|
3742
|
+
}
|
|
3743
|
+
walk(value, depth) {
|
|
3744
|
+
if (depth > 400 || value === null) return;
|
|
3745
|
+
if (typeof value === "string") {
|
|
3746
|
+
if (value.length >= this.deps.prefix.length && value.includes(this.deps.prefix)) this.match(value);
|
|
3747
|
+
return;
|
|
3748
|
+
}
|
|
3749
|
+
if (typeof value !== "object") return;
|
|
3750
|
+
if (Array.isArray(value)) {
|
|
3751
|
+
for (const item of value) this.walk(item, depth + 1);
|
|
3752
|
+
return;
|
|
3753
|
+
}
|
|
3754
|
+
for (const key in value) this.walk(value[key], depth + 1);
|
|
3755
|
+
}
|
|
3756
|
+
match(text) {
|
|
3757
|
+
var _a;
|
|
3758
|
+
this.pattern.lastIndex = 0;
|
|
3759
|
+
for (const found of (_a = text.match(this.pattern)) != null ? _a : []) {
|
|
3760
|
+
if (this.seen.has(found)) continue;
|
|
3761
|
+
if (this.seen.size >= this.limits.maxUrls) return;
|
|
3762
|
+
this.seen.add(found);
|
|
3763
|
+
if (DEVICE_FILE_PATH.test(found.slice(this.deps.prefix.length))) continue;
|
|
3764
|
+
if (!classify(found)) continue;
|
|
3765
|
+
this.queue.push(found);
|
|
3766
|
+
}
|
|
3767
|
+
if (this.queue.length > 0) void this.drain();
|
|
3768
|
+
}
|
|
3769
|
+
async drain() {
|
|
3770
|
+
if (this.running) return;
|
|
3771
|
+
this.running = true;
|
|
3772
|
+
try {
|
|
3773
|
+
while (this.queue.length > 0 && !this.stopped) {
|
|
3774
|
+
const url = this.queue.shift();
|
|
3775
|
+
try {
|
|
3776
|
+
const payload = await this.resolve(url);
|
|
3777
|
+
if (payload && !this.stopped) this.deps.emit(payload);
|
|
3778
|
+
} catch (error) {
|
|
3779
|
+
if (this.deps.debug) console.warn("[anyreplay] could not include", url, error);
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
} finally {
|
|
3783
|
+
this.running = false;
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
async resolve(url) {
|
|
3787
|
+
const kind = classify(url);
|
|
3788
|
+
if (!kind) return null;
|
|
3789
|
+
const response = await this.deps.fetch(url);
|
|
3790
|
+
if (!response.ok) return null;
|
|
3791
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
3792
|
+
if (bytes.length === 0) return null;
|
|
3793
|
+
if (kind.kind === "inline") {
|
|
3794
|
+
if (bytes.length > this.limits[kind.cap]) return this.skip(url, "over its size cap");
|
|
3795
|
+
if (this.inlineSpent + bytes.length > this.limits.inlineBudgetBytes) return this.skip(url, "over the page budget");
|
|
3796
|
+
this.inlineSpent += bytes.length;
|
|
3797
|
+
return { url, data: `data:${kind.mime};base64,${toBase64(bytes)}` };
|
|
3798
|
+
}
|
|
3799
|
+
if (bytes.length > this.limits.maxImageBytes) return this.skip(url, "larger than 3 MB");
|
|
3800
|
+
const sha256 = await sha256Hex(bytes);
|
|
3801
|
+
if (this.confirmed.has(sha256) || await this.upload(sha256, bytes)) {
|
|
3802
|
+
this.confirmed.add(sha256);
|
|
3803
|
+
return { url, sha256 };
|
|
3804
|
+
}
|
|
3805
|
+
return null;
|
|
3806
|
+
}
|
|
3807
|
+
/** Contract §9: ask first, upload only what the project does not hold. */
|
|
3808
|
+
async upload(sha256, bytes) {
|
|
3809
|
+
const { ingestUrl, projectKey, appId } = this.deps;
|
|
3810
|
+
const app = appId ? { appId } : {};
|
|
3811
|
+
const post = (path, body) => this.deps.fetch(`${ingestUrl}${path}`, {
|
|
3812
|
+
method: "POST",
|
|
3813
|
+
headers: { "Content-Type": "application/json" },
|
|
3814
|
+
credentials: "omit",
|
|
3815
|
+
body: JSON.stringify(body)
|
|
3816
|
+
});
|
|
3817
|
+
const known = await post("/v1/ingest/assets/known", { projectKey, hashes: [sha256], ...app });
|
|
3818
|
+
if (!known.ok) {
|
|
3819
|
+
if (known.status === 401 || known.status === 403) this.stop();
|
|
3820
|
+
return false;
|
|
3821
|
+
}
|
|
3822
|
+
const answer = await known.json();
|
|
3823
|
+
if (Array.isArray(answer.known) && answer.known.includes(sha256)) return true;
|
|
3824
|
+
const upload = await post("/v1/ingest/assets", { projectKey, sha256, data: toBase64(bytes), ...app });
|
|
3825
|
+
return upload.ok;
|
|
3826
|
+
}
|
|
3827
|
+
skip(url, why) {
|
|
3828
|
+
if (this.deps.debug) console.warn(`[anyreplay] not included in the replay (${why}): ${url}`);
|
|
3829
|
+
return null;
|
|
3830
|
+
}
|
|
3831
|
+
};
|
|
3832
|
+
function toBase64(bytes) {
|
|
3833
|
+
let binary = "";
|
|
3834
|
+
for (let i = 0; i < bytes.length; i += 32768) {
|
|
3835
|
+
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 32768));
|
|
3836
|
+
}
|
|
3837
|
+
return btoa(binary);
|
|
3838
|
+
}
|
|
3839
|
+
async function sha256Hex(bytes) {
|
|
3840
|
+
try {
|
|
3841
|
+
const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
|
|
3842
|
+
if (subtle) {
|
|
3843
|
+
const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
|
|
3844
|
+
return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
3845
|
+
}
|
|
3846
|
+
} catch (e) {
|
|
3847
|
+
}
|
|
3848
|
+
return sha256HexSync(bytes);
|
|
3849
|
+
}
|
|
3850
|
+
var K = [
|
|
3851
|
+
1116352408,
|
|
3852
|
+
1899447441,
|
|
3853
|
+
3049323471,
|
|
3854
|
+
3921009573,
|
|
3855
|
+
961987163,
|
|
3856
|
+
1508970993,
|
|
3857
|
+
2453635748,
|
|
3858
|
+
2870763221,
|
|
3859
|
+
3624381080,
|
|
3860
|
+
310598401,
|
|
3861
|
+
607225278,
|
|
3862
|
+
1426881987,
|
|
3863
|
+
1925078388,
|
|
3864
|
+
2162078206,
|
|
3865
|
+
2614888103,
|
|
3866
|
+
3248222580,
|
|
3867
|
+
3835390401,
|
|
3868
|
+
4022224774,
|
|
3869
|
+
264347078,
|
|
3870
|
+
604807628,
|
|
3871
|
+
770255983,
|
|
3872
|
+
1249150122,
|
|
3873
|
+
1555081692,
|
|
3874
|
+
1996064986,
|
|
3875
|
+
2554220882,
|
|
3876
|
+
2821834349,
|
|
3877
|
+
2952996808,
|
|
3878
|
+
3210313671,
|
|
3879
|
+
3336571891,
|
|
3880
|
+
3584528711,
|
|
3881
|
+
113926993,
|
|
3882
|
+
338241895,
|
|
3883
|
+
666307205,
|
|
3884
|
+
773529912,
|
|
3885
|
+
1294757372,
|
|
3886
|
+
1396182291,
|
|
3887
|
+
1695183700,
|
|
3888
|
+
1986661051,
|
|
3889
|
+
2177026350,
|
|
3890
|
+
2456956037,
|
|
3891
|
+
2730485921,
|
|
3892
|
+
2820302411,
|
|
3893
|
+
3259730800,
|
|
3894
|
+
3345764771,
|
|
3895
|
+
3516065817,
|
|
3896
|
+
3600352804,
|
|
3897
|
+
4094571909,
|
|
3898
|
+
275423344,
|
|
3899
|
+
430227734,
|
|
3900
|
+
506948616,
|
|
3901
|
+
659060556,
|
|
3902
|
+
883997877,
|
|
3903
|
+
958139571,
|
|
3904
|
+
1322822218,
|
|
3905
|
+
1537002063,
|
|
3906
|
+
1747873779,
|
|
3907
|
+
1955562222,
|
|
3908
|
+
2024104815,
|
|
3909
|
+
2227730452,
|
|
3910
|
+
2361852424,
|
|
3911
|
+
2428436474,
|
|
3912
|
+
2756734187,
|
|
3913
|
+
3204031479,
|
|
3914
|
+
3329325298
|
|
3915
|
+
];
|
|
3916
|
+
function sha256HexSync(bytes) {
|
|
3917
|
+
const length = bytes.length;
|
|
3918
|
+
const padded = new Uint8Array(length + 9 + 63 >>> 6 << 6);
|
|
3919
|
+
padded.set(bytes);
|
|
3920
|
+
padded[length] = 128;
|
|
3921
|
+
const view = new DataView(padded.buffer);
|
|
3922
|
+
view.setUint32(padded.length - 8, Math.floor(length / 536870912));
|
|
3923
|
+
view.setUint32(padded.length - 4, length << 3 >>> 0);
|
|
3924
|
+
const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
|
|
3925
|
+
const w = new Array(64);
|
|
3926
|
+
const rotr = (x, n) => x >>> n | x << 32 - n;
|
|
3927
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
3928
|
+
for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4);
|
|
3929
|
+
for (let i = 16; i < 64; i += 1) {
|
|
3930
|
+
const a2 = w[i - 15];
|
|
3931
|
+
const b2 = w[i - 2];
|
|
3932
|
+
w[i] = w[i - 16] + (rotr(a2, 7) ^ rotr(a2, 18) ^ a2 >>> 3) + w[i - 7] + (rotr(b2, 17) ^ rotr(b2, 19) ^ b2 >>> 10) | 0;
|
|
3933
|
+
}
|
|
3934
|
+
let [a, b, c, d, e, f, g, hh] = h;
|
|
3935
|
+
for (let i = 0; i < 64; i += 1) {
|
|
3936
|
+
const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
|
|
3937
|
+
const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
|
|
3938
|
+
hh = g;
|
|
3939
|
+
g = f;
|
|
3940
|
+
f = e;
|
|
3941
|
+
e = d + t1 | 0;
|
|
3942
|
+
d = c;
|
|
3943
|
+
c = b;
|
|
3944
|
+
b = a;
|
|
3945
|
+
a = t1 + t2 | 0;
|
|
3946
|
+
}
|
|
3947
|
+
h[0] = h[0] + a | 0;
|
|
3948
|
+
h[1] = h[1] + b | 0;
|
|
3949
|
+
h[2] = h[2] + c | 0;
|
|
3950
|
+
h[3] = h[3] + d | 0;
|
|
3951
|
+
h[4] = h[4] + e | 0;
|
|
3952
|
+
h[5] = h[5] + f | 0;
|
|
3953
|
+
h[6] = h[6] + g | 0;
|
|
3954
|
+
h[7] = h[7] + hh | 0;
|
|
3955
|
+
}
|
|
3956
|
+
return h.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3640
3959
|
// src/config.ts
|
|
3641
3960
|
var DEFAULTS = {
|
|
3642
3961
|
ingestUrl: "https://in.anyreplay.com",
|
|
@@ -3652,15 +3971,23 @@ var DEFAULTS = {
|
|
|
3652
3971
|
requireConsent: false,
|
|
3653
3972
|
maxEventsPerChunk: 200,
|
|
3654
3973
|
flushIntervalMs: 5e3,
|
|
3655
|
-
|
|
3974
|
+
// Never: an outage is what the buffer is for. Set a number to give up instead.
|
|
3975
|
+
maxConsecutiveFailures: 0,
|
|
3976
|
+
// The three diagnostic streams. On, because "why did it break" is the
|
|
3977
|
+
// question a replay is opened to answer; each is a documented switch, and
|
|
3978
|
+
// what they can and cannot contain is in diagnostics.ts.
|
|
3979
|
+
recordErrors: true,
|
|
3980
|
+
recordConsole: true,
|
|
3981
|
+
recordNetwork: true,
|
|
3656
3982
|
debug: false
|
|
3657
3983
|
};
|
|
3658
3984
|
var ConfigError = class extends Error {
|
|
3659
3985
|
};
|
|
3660
3986
|
var KEY_PATTERN = /^ar_pk_(live|test)_[0-9a-f]{24}$/;
|
|
3661
3987
|
var MAX_SESSION_CAP = 1e6;
|
|
3988
|
+
var APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;
|
|
3662
3989
|
function resolveOptions(input) {
|
|
3663
|
-
var _a, _b, _c;
|
|
3990
|
+
var _a, _b, _c, _d;
|
|
3664
3991
|
if (!input || typeof input !== "object") throw new ConfigError("anyreplay: options object required");
|
|
3665
3992
|
if (!KEY_PATTERN.test((_a = input.projectKey) != null ? _a : "")) {
|
|
3666
3993
|
throw new ConfigError("anyreplay: projectKey looks wrong \u2014 copy it from the dashboard");
|
|
@@ -3673,53 +4000,42 @@ function resolveOptions(input) {
|
|
|
3673
4000
|
if (cap !== void 0 && !(Number.isInteger(cap) && cap >= 1 && cap <= MAX_SESSION_CAP)) {
|
|
3674
4001
|
throw new ConfigError(`anyreplay: maxSessionsPerMonth must be a whole number between 1 and ${MAX_SESSION_CAP}`);
|
|
3675
4002
|
}
|
|
4003
|
+
const appId = (_c = input.appId) == null ? void 0 : _c.trim();
|
|
4004
|
+
if (input.appId !== void 0 && !(appId && APP_ID_PATTERN.test(appId))) {
|
|
4005
|
+
throw new ConfigError("anyreplay: appId should be an app identifier such as com.example.app");
|
|
4006
|
+
}
|
|
4007
|
+
let assets = input.assets;
|
|
4008
|
+
if (assets !== void 0 && assets !== "reference" && assets !== "inline") {
|
|
4009
|
+
if (typeof console !== "undefined") {
|
|
4010
|
+
console.warn(`[anyreplay] assets ignored: expected 'reference' or 'inline', got ${JSON.stringify(assets)}.`);
|
|
4011
|
+
}
|
|
4012
|
+
assets = void 0;
|
|
4013
|
+
}
|
|
3676
4014
|
return {
|
|
3677
4015
|
...DEFAULTS,
|
|
3678
4016
|
...input,
|
|
4017
|
+
...appId ? { appId } : {},
|
|
4018
|
+
assets: assets != null ? assets : defaultAssetMode(),
|
|
3679
4019
|
projectKey: input.projectKey,
|
|
3680
4020
|
sampleRate,
|
|
3681
|
-
ingestUrl: ((
|
|
4021
|
+
ingestUrl: ((_d = input.ingestUrl) != null ? _d : DEFAULTS.ingestUrl).replace(/\/+$/, ""),
|
|
3682
4022
|
maskAllInputs: readSwitch(input, "maskAllInputs"),
|
|
3683
|
-
maskAllTyping: readSwitch(input, "maskAllTyping")
|
|
4023
|
+
maskAllTyping: readSwitch(input, "maskAllTyping"),
|
|
4024
|
+
recordErrors: readSwitch(input, "recordErrors"),
|
|
4025
|
+
recordConsole: readSwitch(input, "recordConsole"),
|
|
4026
|
+
recordNetwork: readSwitch(input, "recordNetwork")
|
|
3684
4027
|
};
|
|
3685
4028
|
}
|
|
3686
4029
|
function readSwitch(input, name) {
|
|
3687
4030
|
const value = input[name];
|
|
3688
4031
|
if (typeof value === "boolean") return value;
|
|
3689
4032
|
if (value !== void 0 && typeof console !== "undefined") {
|
|
3690
|
-
|
|
4033
|
+
const consequence = name.startsWith("mask") ? "Nothing is masked by this option." : `${name.slice("record".length)} are recorded, as by default.`;
|
|
4034
|
+
console.warn(`[anyreplay] ${name} ignored: expected true or false, got ${typeof value}. ${consequence}`);
|
|
3691
4035
|
}
|
|
3692
4036
|
return DEFAULTS[name];
|
|
3693
4037
|
}
|
|
3694
4038
|
|
|
3695
|
-
// src/events.ts
|
|
3696
|
-
var TRACK_TAG = "anyreplay.track";
|
|
3697
|
-
var NAVIGATE_TAG = "anyreplay.navigate";
|
|
3698
|
-
var EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
3699
|
-
var MAX_PROPERTIES_BYTES = 4 * 1024;
|
|
3700
|
-
function validateTrack(name, properties) {
|
|
3701
|
-
if (typeof name !== "string" || !EVENT_NAME_PATTERN.test(name)) {
|
|
3702
|
-
return `event name must be 1-64 characters of [A-Za-z0-9_.:-], got ${JSON.stringify(name)}`;
|
|
3703
|
-
}
|
|
3704
|
-
if (properties === void 0 || properties === null) return { name, properties: {} };
|
|
3705
|
-
if (typeof properties !== "object" || Array.isArray(properties)) {
|
|
3706
|
-
return `properties for "${name}" must be a plain object`;
|
|
3707
|
-
}
|
|
3708
|
-
let serialised;
|
|
3709
|
-
try {
|
|
3710
|
-
serialised = JSON.stringify(properties);
|
|
3711
|
-
} catch (e) {
|
|
3712
|
-
return `properties for "${name}" are not JSON-serialisable`;
|
|
3713
|
-
}
|
|
3714
|
-
if (serialised === void 0 || byteLength(serialised) > MAX_PROPERTIES_BYTES) {
|
|
3715
|
-
return `properties for "${name}" exceed ${MAX_PROPERTIES_BYTES} bytes once serialised`;
|
|
3716
|
-
}
|
|
3717
|
-
return { name, properties: JSON.parse(serialised) };
|
|
3718
|
-
}
|
|
3719
|
-
function byteLength(text) {
|
|
3720
|
-
return typeof TextEncoder === "function" ? new TextEncoder().encode(text).length : text.length * 3;
|
|
3721
|
-
}
|
|
3722
|
-
|
|
3723
4039
|
// src/masking.ts
|
|
3724
4040
|
var maskValue = (text) => "*".repeat(text.length);
|
|
3725
4041
|
var MASK_EVERY_FIELD = {
|
|
@@ -3809,6 +4125,13 @@ function looksLikeCardNumber(text) {
|
|
|
3809
4125
|
}
|
|
3810
4126
|
return sum % 10 === 0;
|
|
3811
4127
|
}
|
|
4128
|
+
function mayBecomeCardNumber(text) {
|
|
4129
|
+
if (!/^[2-6][0-9 -]*$/.test(text)) return false;
|
|
4130
|
+
const groups = text.split(/[ -]+/);
|
|
4131
|
+
if (groups.slice(0, -1).some((group) => group.length < 4)) return false;
|
|
4132
|
+
const digits = groups.join("").length;
|
|
4133
|
+
return digits > 6 && digits <= 19;
|
|
4134
|
+
}
|
|
3812
4135
|
function isMarkedByCustomer(node, maskClass) {
|
|
3813
4136
|
var _a;
|
|
3814
4137
|
let element = asElement(node);
|
|
@@ -3823,8 +4146,19 @@ function shouldMaskValue(text, node, policy) {
|
|
|
3823
4146
|
if (policy.maskEveryField) return true;
|
|
3824
4147
|
if (isSensitiveField(node)) return true;
|
|
3825
4148
|
if (isMarkedByCustomer(node, policy.maskClass)) return true;
|
|
3826
|
-
return
|
|
4149
|
+
return cardShaped(text);
|
|
3827
4150
|
}
|
|
4151
|
+
var cardShaped = (text) => looksLikeCardNumber(text) || mayBecomeCardNumber(text);
|
|
4152
|
+
var EDITABLE = '[contenteditable]:not([contenteditable="false"])';
|
|
4153
|
+
function insideEditable(node) {
|
|
4154
|
+
var _a;
|
|
4155
|
+
const live = node;
|
|
4156
|
+
const element = (live == null ? void 0 : live.nodeType) === 3 ? (_a = live.parentElement) != null ? _a : null : asElement(live);
|
|
4157
|
+
return typeof (element == null ? void 0 : element.closest) === "function" && element.closest(EDITABLE) !== null;
|
|
4158
|
+
}
|
|
4159
|
+
var isEditableSerialised = (attributes) => attributes.contenteditable !== void 0 && attributes.contenteditable !== "false";
|
|
4160
|
+
var typedCardShaped = (text) => cardShaped(text.replace(/\u00a0/g, " ").trim());
|
|
4161
|
+
var maskText = (text) => text.replace(/\S/g, "*");
|
|
3828
4162
|
var MUTATION = 0;
|
|
3829
4163
|
var INPUT = 5;
|
|
3830
4164
|
var ELEMENT_NODE = 2;
|
|
@@ -3839,23 +4173,27 @@ function maskTextChildren(node) {
|
|
|
3839
4173
|
maskTextChildren(child);
|
|
3840
4174
|
}
|
|
3841
4175
|
}
|
|
3842
|
-
function scrubTree(node, policy, masked) {
|
|
4176
|
+
function scrubTree(node, policy, masked, editable = false) {
|
|
3843
4177
|
var _a, _b;
|
|
3844
4178
|
if (!node) return;
|
|
3845
4179
|
const attributes = (_a = node.attributes) != null ? _a : {};
|
|
3846
4180
|
const inheritedMask = masked || hasClass(attributes, policy.maskClass);
|
|
4181
|
+
const inheritedEditable = editable || node.type === ELEMENT_NODE && isEditableSerialised(attributes);
|
|
4182
|
+
if (editable && node.type === TEXT_NODE && typeof node.textContent === "string" && typedCardShaped(node.textContent)) {
|
|
4183
|
+
node.textContent = maskText(node.textContent);
|
|
4184
|
+
}
|
|
3847
4185
|
if (node.type === ELEMENT_NODE && node.tagName && FIELD_TAGS.has(node.tagName.toLowerCase())) {
|
|
3848
4186
|
const value = attributes.value;
|
|
3849
|
-
const shouldMask = policy.maskEveryField || inheritedMask || isSensitiveSerialised(attributes) || typeof value === "string" &&
|
|
4187
|
+
const shouldMask = policy.maskEveryField || inheritedMask || isSensitiveSerialised(attributes) || typeof value === "string" && cardShaped(value);
|
|
3850
4188
|
if (shouldMask) {
|
|
3851
4189
|
if (typeof value === "string" && value !== "") attributes.value = maskValue(value);
|
|
3852
4190
|
if (node.tagName.toLowerCase() === "textarea") maskTextChildren(node);
|
|
3853
4191
|
}
|
|
3854
4192
|
}
|
|
3855
|
-
for (const child of (_b = node.childNodes) != null ? _b : []) scrubTree(child, policy, inheritedMask);
|
|
4193
|
+
for (const child of (_b = node.childNodes) != null ? _b : []) scrubTree(child, policy, inheritedMask, inheritedEditable);
|
|
3856
4194
|
}
|
|
3857
4195
|
function scrubEvent(event, getNode, policy) {
|
|
3858
|
-
var _a, _b, _c, _d, _e;
|
|
4196
|
+
var _a, _b, _c, _d, _e, _f;
|
|
3859
4197
|
if (!event.data) return;
|
|
3860
4198
|
if (event.type === 2) {
|
|
3861
4199
|
scrubTree(event.data.node, policy, false);
|
|
@@ -3880,10 +4218,304 @@ function scrubEvent(event, getNode, policy) {
|
|
|
3880
4218
|
}
|
|
3881
4219
|
for (const add of (_e = data.adds) != null ? _e : []) {
|
|
3882
4220
|
const parent = typeof (add == null ? void 0 : add.parentId) === "number" ? getNode(add.parentId) : null;
|
|
3883
|
-
scrubTree(add == null ? void 0 : add.node, policy, isMarkedByCustomer(parent, policy.maskClass));
|
|
4221
|
+
scrubTree(add == null ? void 0 : add.node, policy, isMarkedByCustomer(parent, policy.maskClass), insideEditable(parent));
|
|
4222
|
+
}
|
|
4223
|
+
for (const change of (_f = data.texts) != null ? _f : []) {
|
|
4224
|
+
if (typeof (change == null ? void 0 : change.value) !== "string" || !typedCardShaped(change.value)) continue;
|
|
4225
|
+
if (typeof change.id === "number" && insideEditable(getNode(change.id))) change.value = maskText(change.value);
|
|
3884
4226
|
}
|
|
3885
4227
|
}
|
|
3886
4228
|
|
|
4229
|
+
// src/diagnostics.ts
|
|
4230
|
+
var ERROR_TAG = "anyreplay.error";
|
|
4231
|
+
var CONSOLE_TAG = "anyreplay.console";
|
|
4232
|
+
var NETWORK_TAG = "anyreplay.network";
|
|
4233
|
+
var LIMITS = { error: 100, console: 200, network: 500 };
|
|
4234
|
+
var MAX_MESSAGE = 1e3;
|
|
4235
|
+
var MAX_STACK = 4e3;
|
|
4236
|
+
var MAX_URL = 500;
|
|
4237
|
+
var SECRET_KEY = /^(?:.*[_-])?(?:token|secret|password|passwd|pwd|key|apikey|api_key|auth|authorization|session|sid|jwt|code|otp|signature|sig|credential|access|refresh)(?:[_-].*)?$/i;
|
|
4238
|
+
var EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
|
|
4239
|
+
var OPAQUE = /\b(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{24,}\b/g;
|
|
4240
|
+
var DIGITS = /\b[0-9][0-9 -]{11,22}[0-9]\b/g;
|
|
4241
|
+
var REDACTED = "[redacted]";
|
|
4242
|
+
function redactText(text) {
|
|
4243
|
+
return text.replace(EMAIL, REDACTED).replace(DIGITS, (match) => looksLikeCardNumber(match.trim()) ? REDACTED : match).replace(OPAQUE, REDACTED);
|
|
4244
|
+
}
|
|
4245
|
+
function redactUrl(raw) {
|
|
4246
|
+
const text = raw.length > MAX_URL ? raw.slice(0, MAX_URL) : raw;
|
|
4247
|
+
let parsed;
|
|
4248
|
+
try {
|
|
4249
|
+
parsed = new URL(text, typeof location !== "undefined" ? location.href : "http://localhost");
|
|
4250
|
+
} catch (e) {
|
|
4251
|
+
return redactText(text);
|
|
4252
|
+
}
|
|
4253
|
+
for (const key of [...parsed.searchParams.keys()]) {
|
|
4254
|
+
if (SECRET_KEY.test(key)) parsed.searchParams.set(key, REDACTED);
|
|
4255
|
+
}
|
|
4256
|
+
parsed.hash = "";
|
|
4257
|
+
return redactText(`${parsed.origin}${parsed.pathname}${parsed.search}`);
|
|
4258
|
+
}
|
|
4259
|
+
var clip = (text, max) => text.length > max ? `${text.slice(0, max)}\u2026` : text;
|
|
4260
|
+
function describe(value) {
|
|
4261
|
+
if (typeof value === "string") return value;
|
|
4262
|
+
if (value === null) return "null";
|
|
4263
|
+
if (value === void 0) return "undefined";
|
|
4264
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
|
|
4265
|
+
if (value instanceof Error) return `${value.name}: ${value.message}`;
|
|
4266
|
+
try {
|
|
4267
|
+
const json = JSON.stringify(value);
|
|
4268
|
+
return json === void 0 ? Object.prototype.toString.call(value) : json;
|
|
4269
|
+
} catch (e) {
|
|
4270
|
+
return Object.prototype.toString.call(value);
|
|
4271
|
+
}
|
|
4272
|
+
}
|
|
4273
|
+
function errorPayload(kind, value, where) {
|
|
4274
|
+
const error = value instanceof Error ? value : null;
|
|
4275
|
+
const message = error ? `${error.name}: ${error.message}` : describe(value);
|
|
4276
|
+
const payload = { message: clip(redactText(message) || "error", MAX_MESSAGE), kind };
|
|
4277
|
+
if (error) {
|
|
4278
|
+
payload.name = error.name;
|
|
4279
|
+
if (typeof error.stack === "string" && error.stack.length > 0) {
|
|
4280
|
+
payload.stack = clip(redactText(error.stack), MAX_STACK);
|
|
4281
|
+
}
|
|
4282
|
+
}
|
|
4283
|
+
if (typeof (where == null ? void 0 : where.source) === "string" && where.source.length > 0) payload.source = redactUrl(where.source);
|
|
4284
|
+
if (typeof (where == null ? void 0 : where.line) === "number" && Number.isFinite(where.line)) payload.line = where.line;
|
|
4285
|
+
if (typeof (where == null ? void 0 : where.column) === "number" && Number.isFinite(where.column)) payload.column = where.column;
|
|
4286
|
+
return payload;
|
|
4287
|
+
}
|
|
4288
|
+
function consolePayload(level, args) {
|
|
4289
|
+
const joined = args.map(describe).join(" ");
|
|
4290
|
+
return { level, message: clip(redactText(joined), MAX_MESSAGE) };
|
|
4291
|
+
}
|
|
4292
|
+
function methodOf(value) {
|
|
4293
|
+
const text = typeof value === "string" ? value.toUpperCase() : "GET";
|
|
4294
|
+
return /^[A-Z]{3,10}$/.test(text) ? text : "GET";
|
|
4295
|
+
}
|
|
4296
|
+
function startDiagnostics(options) {
|
|
4297
|
+
const undo = [];
|
|
4298
|
+
const counts = { error: 0, console: 0, network: 0 };
|
|
4299
|
+
let flagged = false;
|
|
4300
|
+
const budget = (stream) => {
|
|
4301
|
+
if (counts[stream] >= LIMITS[stream]) return false;
|
|
4302
|
+
counts[stream] += 1;
|
|
4303
|
+
return true;
|
|
4304
|
+
};
|
|
4305
|
+
const flag = () => {
|
|
4306
|
+
if (flagged) return;
|
|
4307
|
+
flagged = true;
|
|
4308
|
+
try {
|
|
4309
|
+
options.onError();
|
|
4310
|
+
} catch (e) {
|
|
4311
|
+
}
|
|
4312
|
+
};
|
|
4313
|
+
const send = (tag, payload) => {
|
|
4314
|
+
try {
|
|
4315
|
+
options.emit(tag, payload);
|
|
4316
|
+
} catch (e) {
|
|
4317
|
+
}
|
|
4318
|
+
};
|
|
4319
|
+
let ingestOrigin = null;
|
|
4320
|
+
try {
|
|
4321
|
+
ingestOrigin = new URL(options.ingestUrl).origin;
|
|
4322
|
+
} catch (e) {
|
|
4323
|
+
}
|
|
4324
|
+
const ours = (url) => ingestOrigin !== null && url.startsWith(ingestOrigin);
|
|
4325
|
+
if (options.errors && typeof window !== "undefined") {
|
|
4326
|
+
const onError = (event) => {
|
|
4327
|
+
var _a;
|
|
4328
|
+
flag();
|
|
4329
|
+
if (!budget("error")) return;
|
|
4330
|
+
send(ERROR_TAG, errorPayload("error", (_a = event.error) != null ? _a : event.message, {
|
|
4331
|
+
source: event.filename,
|
|
4332
|
+
line: event.lineno,
|
|
4333
|
+
column: event.colno
|
|
4334
|
+
}));
|
|
4335
|
+
};
|
|
4336
|
+
const onRejection = (event) => {
|
|
4337
|
+
flag();
|
|
4338
|
+
if (!budget("error")) return;
|
|
4339
|
+
send(ERROR_TAG, errorPayload("rejection", event.reason));
|
|
4340
|
+
};
|
|
4341
|
+
window.addEventListener("error", onError, { capture: true });
|
|
4342
|
+
window.addEventListener("unhandledrejection", onRejection, { capture: true });
|
|
4343
|
+
undo.push(() => {
|
|
4344
|
+
window.removeEventListener("error", onError, { capture: true });
|
|
4345
|
+
window.removeEventListener("unhandledrejection", onRejection, { capture: true });
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
4348
|
+
if (options.console && typeof console !== "undefined") {
|
|
4349
|
+
for (const level of ["error", "warn"]) {
|
|
4350
|
+
const original = console[level];
|
|
4351
|
+
if (typeof original !== "function") continue;
|
|
4352
|
+
const patched = function patchedConsole(...args) {
|
|
4353
|
+
try {
|
|
4354
|
+
const first = args[0];
|
|
4355
|
+
const own = typeof first === "string" && first.startsWith("[anyreplay]");
|
|
4356
|
+
if (!own && budget("console")) send(CONSOLE_TAG, consolePayload(level, args));
|
|
4357
|
+
} catch (e) {
|
|
4358
|
+
}
|
|
4359
|
+
original.apply(this, args);
|
|
4360
|
+
};
|
|
4361
|
+
console[level] = patched;
|
|
4362
|
+
undo.push(() => {
|
|
4363
|
+
console[level] = original;
|
|
4364
|
+
});
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
if (options.network && typeof window !== "undefined") {
|
|
4368
|
+
const originalFetch = window.fetch;
|
|
4369
|
+
if (typeof originalFetch === "function") {
|
|
4370
|
+
const patchedFetch = function patchedFetch2(input, init) {
|
|
4371
|
+
var _a, _b;
|
|
4372
|
+
let url = "";
|
|
4373
|
+
let method = "GET";
|
|
4374
|
+
try {
|
|
4375
|
+
url = typeof input === "string" ? input : input instanceof URL ? input.href : (_a = input.url) != null ? _a : "";
|
|
4376
|
+
method = methodOf((_b = init == null ? void 0 : init.method) != null ? _b : input == null ? void 0 : input.method);
|
|
4377
|
+
} catch (e) {
|
|
4378
|
+
}
|
|
4379
|
+
const started = Date.now();
|
|
4380
|
+
const result = originalFetch.call(window, input, init);
|
|
4381
|
+
if (!url || ours(url)) return result;
|
|
4382
|
+
return result.then(
|
|
4383
|
+
(response) => {
|
|
4384
|
+
try {
|
|
4385
|
+
if (budget("network")) {
|
|
4386
|
+
if (response.status >= 400) flag();
|
|
4387
|
+
send(NETWORK_TAG, {
|
|
4388
|
+
url: redactUrl(url),
|
|
4389
|
+
method,
|
|
4390
|
+
status: response.status,
|
|
4391
|
+
durationMs: Date.now() - started,
|
|
4392
|
+
initiator: "fetch"
|
|
4393
|
+
});
|
|
4394
|
+
}
|
|
4395
|
+
} catch (e) {
|
|
4396
|
+
}
|
|
4397
|
+
return response;
|
|
4398
|
+
},
|
|
4399
|
+
(reason) => {
|
|
4400
|
+
try {
|
|
4401
|
+
flag();
|
|
4402
|
+
if (budget("network")) {
|
|
4403
|
+
send(NETWORK_TAG, {
|
|
4404
|
+
url: redactUrl(url),
|
|
4405
|
+
method,
|
|
4406
|
+
status: 0,
|
|
4407
|
+
durationMs: Date.now() - started,
|
|
4408
|
+
initiator: "fetch",
|
|
4409
|
+
failed: true
|
|
4410
|
+
});
|
|
4411
|
+
}
|
|
4412
|
+
} catch (e) {
|
|
4413
|
+
}
|
|
4414
|
+
throw reason;
|
|
4415
|
+
}
|
|
4416
|
+
);
|
|
4417
|
+
};
|
|
4418
|
+
window.fetch = patchedFetch;
|
|
4419
|
+
undo.push(() => {
|
|
4420
|
+
window.fetch = originalFetch;
|
|
4421
|
+
});
|
|
4422
|
+
}
|
|
4423
|
+
const Xhr = window.XMLHttpRequest;
|
|
4424
|
+
if (typeof Xhr === "function") {
|
|
4425
|
+
const open = Xhr.prototype.open;
|
|
4426
|
+
const sendMethod = Xhr.prototype.send;
|
|
4427
|
+
const seen = /* @__PURE__ */ new WeakMap();
|
|
4428
|
+
Xhr.prototype.open = function patchedOpen(method, url, ...rest) {
|
|
4429
|
+
try {
|
|
4430
|
+
const href = typeof url === "string" ? url : url.href;
|
|
4431
|
+
if (href && !ours(href)) seen.set(this, { url: href, method: methodOf(method), started: 0 });
|
|
4432
|
+
} catch (e) {
|
|
4433
|
+
}
|
|
4434
|
+
return open.call(this, method, url, ...rest);
|
|
4435
|
+
};
|
|
4436
|
+
Xhr.prototype.send = function patchedSend(...args) {
|
|
4437
|
+
try {
|
|
4438
|
+
const request = seen.get(this);
|
|
4439
|
+
if (request) {
|
|
4440
|
+
request.started = Date.now();
|
|
4441
|
+
const done = (failed) => {
|
|
4442
|
+
try {
|
|
4443
|
+
if (failed || this.status >= 400) flag();
|
|
4444
|
+
if (!budget("network")) return;
|
|
4445
|
+
send(NETWORK_TAG, {
|
|
4446
|
+
url: redactUrl(request.url),
|
|
4447
|
+
method: request.method,
|
|
4448
|
+
status: this.status || 0,
|
|
4449
|
+
durationMs: Date.now() - request.started,
|
|
4450
|
+
initiator: "xhr",
|
|
4451
|
+
...failed ? { failed: true } : {}
|
|
4452
|
+
});
|
|
4453
|
+
} catch (e) {
|
|
4454
|
+
}
|
|
4455
|
+
};
|
|
4456
|
+
this.addEventListener("load", () => done(false));
|
|
4457
|
+
this.addEventListener("error", () => done(true));
|
|
4458
|
+
this.addEventListener("timeout", () => done(true));
|
|
4459
|
+
this.addEventListener("abort", () => done(true));
|
|
4460
|
+
}
|
|
4461
|
+
} catch (e) {
|
|
4462
|
+
}
|
|
4463
|
+
return sendMethod.call(this, ...args);
|
|
4464
|
+
};
|
|
4465
|
+
undo.push(() => {
|
|
4466
|
+
Xhr.prototype.open = open;
|
|
4467
|
+
Xhr.prototype.send = sendMethod;
|
|
4468
|
+
});
|
|
4469
|
+
}
|
|
4470
|
+
}
|
|
4471
|
+
let stopped = false;
|
|
4472
|
+
return {
|
|
4473
|
+
error: (value) => {
|
|
4474
|
+
if (stopped) return;
|
|
4475
|
+
flag();
|
|
4476
|
+
if (budget("error")) send(ERROR_TAG, errorPayload("error", value));
|
|
4477
|
+
},
|
|
4478
|
+
stop: () => {
|
|
4479
|
+
if (stopped) return;
|
|
4480
|
+
stopped = true;
|
|
4481
|
+
for (let i = undo.length - 1; i >= 0; i -= 1) {
|
|
4482
|
+
try {
|
|
4483
|
+
undo[i]();
|
|
4484
|
+
} catch (e) {
|
|
4485
|
+
}
|
|
4486
|
+
}
|
|
4487
|
+
}
|
|
4488
|
+
};
|
|
4489
|
+
}
|
|
4490
|
+
|
|
4491
|
+
// src/events.ts
|
|
4492
|
+
var TRACK_TAG = "anyreplay.track";
|
|
4493
|
+
var NAVIGATE_TAG = "anyreplay.navigate";
|
|
4494
|
+
var EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
|
|
4495
|
+
var MAX_PROPERTIES_BYTES = 4 * 1024;
|
|
4496
|
+
function validateTrack(name, properties) {
|
|
4497
|
+
if (typeof name !== "string" || !EVENT_NAME_PATTERN.test(name)) {
|
|
4498
|
+
return `event name must be 1-64 characters of [A-Za-z0-9_.:-], got ${JSON.stringify(name)}`;
|
|
4499
|
+
}
|
|
4500
|
+
if (properties === void 0 || properties === null) return { name, properties: {} };
|
|
4501
|
+
if (typeof properties !== "object" || Array.isArray(properties)) {
|
|
4502
|
+
return `properties for "${name}" must be a plain object`;
|
|
4503
|
+
}
|
|
4504
|
+
let serialised;
|
|
4505
|
+
try {
|
|
4506
|
+
serialised = JSON.stringify(properties);
|
|
4507
|
+
} catch (e) {
|
|
4508
|
+
return `properties for "${name}" are not JSON-serialisable`;
|
|
4509
|
+
}
|
|
4510
|
+
if (serialised === void 0 || byteLength(serialised) > MAX_PROPERTIES_BYTES) {
|
|
4511
|
+
return `properties for "${name}" exceed ${MAX_PROPERTIES_BYTES} bytes once serialised`;
|
|
4512
|
+
}
|
|
4513
|
+
return { name, properties: JSON.parse(serialised) };
|
|
4514
|
+
}
|
|
4515
|
+
function byteLength(text) {
|
|
4516
|
+
return typeof TextEncoder === "function" ? new TextEncoder().encode(text).length : text.length * 3;
|
|
4517
|
+
}
|
|
4518
|
+
|
|
3887
4519
|
// src/sampling.ts
|
|
3888
4520
|
function hashToUnitInterval(input) {
|
|
3889
4521
|
let hash = 2166136261;
|
|
@@ -3901,6 +4533,17 @@ function isSampledIn(visitorId, sampleRate) {
|
|
|
3901
4533
|
|
|
3902
4534
|
// src/transport.ts
|
|
3903
4535
|
var MAX_STALE_RECOVERIES = 3;
|
|
4536
|
+
var EVENT_FULL_SNAPSHOT2 = 2;
|
|
4537
|
+
var EVENT_INCREMENTAL2 = 3;
|
|
4538
|
+
var MAX_BODY_BYTES = 512 * 1024;
|
|
4539
|
+
var TARGET_CHUNK_BYTES = 256 * 1024;
|
|
4540
|
+
var MIN_BODY_BYTES = 8 * 1024;
|
|
4541
|
+
var ENVELOPE_SLACK_BYTES = 1024;
|
|
4542
|
+
var MAX_CHUNKS_PER_FLUSH = 20;
|
|
4543
|
+
var BACKOFF_BASE_MS = 2e3;
|
|
4544
|
+
var BACKOFF_MAX_MS = 6e4;
|
|
4545
|
+
var MAX_RETRY_AFTER_MS = 10 * 6e4;
|
|
4546
|
+
var MAX_CONSECUTIVE_REFUSALS = 3;
|
|
3904
4547
|
async function staleSeqHint(response, sent) {
|
|
3905
4548
|
try {
|
|
3906
4549
|
if (typeof response.json !== "function") return void 0;
|
|
@@ -3912,10 +4555,40 @@ async function staleSeqHint(response, sent) {
|
|
|
3912
4555
|
return void 0;
|
|
3913
4556
|
}
|
|
3914
4557
|
}
|
|
4558
|
+
function retryAfterMs(response, now = Date.now()) {
|
|
4559
|
+
var _a, _b;
|
|
4560
|
+
let raw;
|
|
4561
|
+
try {
|
|
4562
|
+
raw = (_b = (_a = response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "Retry-After");
|
|
4563
|
+
} catch (e) {
|
|
4564
|
+
return void 0;
|
|
4565
|
+
}
|
|
4566
|
+
if (!raw) return void 0;
|
|
4567
|
+
const text = raw.trim();
|
|
4568
|
+
if (/^\d+(\.\d+)?$/.test(text)) return Math.min(MAX_RETRY_AFTER_MS, Number(text) * 1e3);
|
|
4569
|
+
const date = Date.parse(text);
|
|
4570
|
+
if (Number.isNaN(date)) return void 0;
|
|
4571
|
+
return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - now));
|
|
4572
|
+
}
|
|
4573
|
+
function utf8Length(text) {
|
|
4574
|
+
let bytes = 0;
|
|
4575
|
+
for (let i = 0; i < text.length; i += 1) {
|
|
4576
|
+
const code = text.charCodeAt(i);
|
|
4577
|
+
if (code < 128) bytes += 1;
|
|
4578
|
+
else if (code < 2048) bytes += 2;
|
|
4579
|
+
else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
|
|
4580
|
+
const next = text.charCodeAt(i + 1);
|
|
4581
|
+
if (next >= 56320 && next <= 57343) {
|
|
4582
|
+
bytes += 4;
|
|
4583
|
+
i += 1;
|
|
4584
|
+
} else bytes += 3;
|
|
4585
|
+
} else bytes += 3;
|
|
4586
|
+
}
|
|
4587
|
+
return bytes;
|
|
4588
|
+
}
|
|
3915
4589
|
var KEEPALIVE_BODY_LIMIT_BYTES = 56 * 1024;
|
|
3916
4590
|
function withinKeepaliveLimit(payload) {
|
|
3917
|
-
|
|
3918
|
-
return bytes <= KEEPALIVE_BODY_LIMIT_BYTES;
|
|
4591
|
+
return utf8Length(payload) <= KEEPALIVE_BODY_LIMIT_BYTES;
|
|
3919
4592
|
}
|
|
3920
4593
|
var Transport = class {
|
|
3921
4594
|
constructor(options, context, hooks = {}) {
|
|
@@ -3923,27 +4596,52 @@ var Transport = class {
|
|
|
3923
4596
|
this.context = context;
|
|
3924
4597
|
this.hooks = hooks;
|
|
3925
4598
|
__publicField(this, "buffer", []);
|
|
4599
|
+
/** The chunk being delivered: built, numbered, maybe already attempted. */
|
|
4600
|
+
__publicField(this, "pending");
|
|
4601
|
+
/** The next unused chunk number. */
|
|
3926
4602
|
__publicField(this, "seq", 0);
|
|
3927
4603
|
__publicField(this, "consecutiveFailures", 0);
|
|
4604
|
+
__publicField(this, "consecutiveRefusals", 0);
|
|
3928
4605
|
__publicField(this, "state", "open");
|
|
3929
4606
|
__publicField(this, "timer");
|
|
4607
|
+
__publicField(this, "retryTimer");
|
|
3930
4608
|
__publicField(this, "pendingMeta");
|
|
3931
4609
|
__publicField(this, "flags", {});
|
|
3932
4610
|
__publicField(this, "inFlight", false);
|
|
3933
|
-
/** The chunk number of the last request that left, to tell a retry from a first attempt. */
|
|
3934
|
-
__publicField(this, "lastAttemptedSeq", -1);
|
|
3935
4611
|
__publicField(this, "staleRecoveries", 0);
|
|
4612
|
+
/** Our own backoff after a failure. Cleared when the network comes back. */
|
|
4613
|
+
__publicField(this, "backoffUntil", 0);
|
|
4614
|
+
/** What the server asked for in `Retry-After`. Not cleared by anything but time. */
|
|
4615
|
+
__publicField(this, "serverWaitUntil", 0);
|
|
4616
|
+
/** Largest body this transport will build; lowered by a `413`. */
|
|
4617
|
+
__publicField(this, "bodyLimit", MAX_BODY_BYTES);
|
|
4618
|
+
__publicField(this, "targetBytes", TARGET_CHUNK_BYTES);
|
|
4619
|
+
/**
|
|
4620
|
+
* Set when events were dropped and a full snapshot has been asked for:
|
|
4621
|
+
* DOM changes are not kept until it arrives.
|
|
4622
|
+
*/
|
|
4623
|
+
__publicField(this, "awaitingSnapshot", false);
|
|
4624
|
+
/** Serialised events, so a retry or a re-split does not stringify a snapshot twice. */
|
|
4625
|
+
__publicField(this, "serialised", /* @__PURE__ */ new WeakMap());
|
|
3936
4626
|
var _a;
|
|
3937
4627
|
this.seq = (_a = context.initialSeq) != null ? _a : 0;
|
|
3938
4628
|
}
|
|
3939
4629
|
get isStopped() {
|
|
3940
4630
|
return this.state === "stopped";
|
|
3941
4631
|
}
|
|
4632
|
+
/** Events not yet delivered, including a chunk waiting to be retried. */
|
|
3942
4633
|
get bufferedCount() {
|
|
3943
|
-
|
|
4634
|
+
var _a, _b;
|
|
4635
|
+
return this.buffer.length + ((_b = (_a = this.pending) == null ? void 0 : _a.events.length) != null ? _b : 0);
|
|
3944
4636
|
}
|
|
4637
|
+
/** The number the next undelivered chunk will carry — the count of chunks delivered, for a fresh session. */
|
|
3945
4638
|
get sentChunks() {
|
|
3946
|
-
|
|
4639
|
+
var _a, _b;
|
|
4640
|
+
return (_b = (_a = this.pending) == null ? void 0 : _a.seq) != null ? _b : this.seq;
|
|
4641
|
+
}
|
|
4642
|
+
/** The most events kept while deliveries fail. Older ones are dropped first. */
|
|
4643
|
+
get bufferLimit() {
|
|
4644
|
+
return this.options.maxEventsPerChunk * 10;
|
|
3947
4645
|
}
|
|
3948
4646
|
/** Attaches metadata to the next chunk. Sent once, with the session's first chunk. */
|
|
3949
4647
|
setMeta(meta) {
|
|
@@ -3962,22 +4660,131 @@ var Transport = class {
|
|
|
3962
4660
|
var _a, _b;
|
|
3963
4661
|
this.state = "stopped";
|
|
3964
4662
|
if (this.timer) clearInterval(this.timer);
|
|
4663
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
3965
4664
|
this.timer = void 0;
|
|
4665
|
+
this.retryTimer = void 0;
|
|
3966
4666
|
this.buffer = [];
|
|
4667
|
+
this.pending = void 0;
|
|
3967
4668
|
(_b = (_a = this.hooks).onStopped) == null ? void 0 : _b.call(_a, reason);
|
|
3968
4669
|
}
|
|
3969
4670
|
push(event) {
|
|
3970
4671
|
if (this.state === "stopped") return;
|
|
4672
|
+
if (this.awaitingSnapshot) {
|
|
4673
|
+
if (event.type === EVENT_INCREMENTAL2) return;
|
|
4674
|
+
if (event.type === EVENT_FULL_SNAPSHOT2) this.awaitingSnapshot = false;
|
|
4675
|
+
}
|
|
3971
4676
|
this.buffer.push(event);
|
|
4677
|
+
if (this.bufferedCount > this.bufferLimit) this.dropOldest(this.bufferedCount - this.bufferLimit);
|
|
3972
4678
|
if (this.buffer.length >= this.options.maxEventsPerChunk) void this.flush();
|
|
3973
4679
|
}
|
|
3974
|
-
|
|
4680
|
+
/**
|
|
4681
|
+
* The network is back (`online`), or the page is visible again: try now
|
|
4682
|
+
* rather than at the end of the backoff. A server's `Retry-After` still
|
|
4683
|
+
* holds — it was not about our connection.
|
|
4684
|
+
*/
|
|
4685
|
+
resume() {
|
|
4686
|
+
if (this.state === "stopped") return;
|
|
4687
|
+
this.backoffUntil = 0;
|
|
4688
|
+
void this.flush();
|
|
4689
|
+
}
|
|
4690
|
+
/**
|
|
4691
|
+
* Sends whatever is buffered, as as many chunks as it takes. Safe to call
|
|
4692
|
+
* when there is nothing to send, and while waiting out a failure, when it
|
|
4693
|
+
* does nothing. `force` skips our own backoff (an explicit `flush()` from
|
|
4694
|
+
* the page) but never the server's `Retry-After`.
|
|
4695
|
+
*/
|
|
4696
|
+
async flush(force = false) {
|
|
4697
|
+
var _a;
|
|
4698
|
+
if (this.state === "stopped" || this.inFlight) return;
|
|
4699
|
+
const now = Date.now();
|
|
4700
|
+
if (now < this.serverWaitUntil) return;
|
|
4701
|
+
if (!force && now < this.backoffUntil) return;
|
|
4702
|
+
if (!this.pending && this.buffer.length === 0) return;
|
|
4703
|
+
this.inFlight = true;
|
|
4704
|
+
try {
|
|
4705
|
+
for (let sent = 0, tries = 0; sent < MAX_CHUNKS_PER_FLUSH && tries < MAX_CHUNKS_PER_FLUSH * 3 && this.state === "open"; tries += 1) {
|
|
4706
|
+
const chunk = (_a = this.pending) != null ? _a : this.nextChunk(this.bodyLimit, this.targetBytes);
|
|
4707
|
+
if (!chunk) break;
|
|
4708
|
+
this.pending = chunk;
|
|
4709
|
+
const outcome = await this.send(chunk);
|
|
4710
|
+
if (outcome === "sent") sent += 1;
|
|
4711
|
+
else if (outcome !== "again") break;
|
|
4712
|
+
}
|
|
4713
|
+
} finally {
|
|
4714
|
+
this.inFlight = false;
|
|
4715
|
+
}
|
|
4716
|
+
}
|
|
4717
|
+
/**
|
|
4718
|
+
* Builds the next chunk from the front of the buffer: at most
|
|
4719
|
+
* `maxEventsPerChunk` events and `target` bytes, or one event alone up to
|
|
4720
|
+
* `limit`. An event too large for any chunk is dropped here — it would be
|
|
4721
|
+
* refused, and refused again, and hold up everything behind it. For a
|
|
4722
|
+
* beacon (`forBeacon`) it is left where it is instead: an ordinary request
|
|
4723
|
+
* can still carry what a beacon cannot.
|
|
4724
|
+
*/
|
|
4725
|
+
nextChunk(limit, target, forBeacon = false) {
|
|
4726
|
+
const envelope = utf8Length(this.envelope(this.seq)) + ENVELOPE_SLACK_BYTES;
|
|
4727
|
+
const events = [];
|
|
4728
|
+
const parts = [];
|
|
4729
|
+
let bytes = envelope;
|
|
4730
|
+
while (this.buffer.length > 0 && events.length < this.options.maxEventsPerChunk) {
|
|
4731
|
+
const event = this.buffer[0];
|
|
4732
|
+
const part = this.serialise(event);
|
|
4733
|
+
const size = utf8Length(part) + 1;
|
|
4734
|
+
if (events.length === 0 && bytes + size > limit) {
|
|
4735
|
+
if (forBeacon) return void 0;
|
|
4736
|
+
this.buffer.shift();
|
|
4737
|
+
if (!this.dropOversized(event, size)) return void 0;
|
|
4738
|
+
continue;
|
|
4739
|
+
}
|
|
4740
|
+
if (events.length > 0 && bytes + size > target) break;
|
|
4741
|
+
events.push(event);
|
|
4742
|
+
parts.push(part);
|
|
4743
|
+
bytes += size;
|
|
4744
|
+
this.buffer.shift();
|
|
4745
|
+
}
|
|
4746
|
+
if (events.length === 0) return void 0;
|
|
4747
|
+
const chunk = { seq: this.seq, events, parts, attempts: 0 };
|
|
4748
|
+
this.seq += 1;
|
|
4749
|
+
return chunk;
|
|
4750
|
+
}
|
|
4751
|
+
/**
|
|
4752
|
+
* An event that no chunk can carry. A DOM change or a custom event is
|
|
4753
|
+
* dropped, and the DOM is re-sent whole if the dropped one described it.
|
|
4754
|
+
* A full snapshot that alone exceeds the limit is a page this transport
|
|
4755
|
+
* cannot record — re-taking it would only be refused again — so recording
|
|
4756
|
+
* stops instead of uploading a snapshot every few seconds forever.
|
|
4757
|
+
*/
|
|
4758
|
+
dropOversized(event, bytes) {
|
|
4759
|
+
var _a, _b, _c, _d;
|
|
4760
|
+
const kb = Math.ceil(bytes / 1024);
|
|
4761
|
+
if (event.type === EVENT_FULL_SNAPSHOT2) {
|
|
4762
|
+
(_b = (_a = this.hooks).onWarning) == null ? void 0 : _b.call(_a, `page snapshot is ${kb} KB, over the ${Math.floor(this.bodyLimit / 1024)} KB ingest accepts`);
|
|
4763
|
+
this.stop("snapshot_too_large");
|
|
4764
|
+
return false;
|
|
4765
|
+
}
|
|
4766
|
+
(_d = (_c = this.hooks).onWarning) == null ? void 0 : _d.call(_c, `dropped one event of ${kb} KB: larger than ingest accepts`);
|
|
4767
|
+
if (event.type === EVENT_INCREMENTAL2) this.afterGap();
|
|
4768
|
+
return true;
|
|
4769
|
+
}
|
|
4770
|
+
serialise(event) {
|
|
4771
|
+
let part = this.serialised.get(event);
|
|
4772
|
+
if (part === void 0) {
|
|
4773
|
+
part = JSON.stringify(event);
|
|
4774
|
+
this.serialised.set(event, part);
|
|
4775
|
+
}
|
|
4776
|
+
return part;
|
|
4777
|
+
}
|
|
4778
|
+
/** Everything in a chunk's body except its events, as a JSON object. */
|
|
4779
|
+
envelope(seq) {
|
|
3975
4780
|
return JSON.stringify({
|
|
3976
4781
|
projectKey: this.context.projectKey,
|
|
3977
4782
|
sessionId: this.context.sessionId,
|
|
3978
4783
|
visitorId: this.context.visitorId,
|
|
3979
|
-
seq
|
|
3980
|
-
|
|
4784
|
+
seq,
|
|
4785
|
+
// The app this page runs inside, on every request: ingest reads it only
|
|
4786
|
+
// when the request has no real web origin. See `appId` in config.ts.
|
|
4787
|
+
...this.options.appId ? { appId: this.options.appId } : {},
|
|
3981
4788
|
...this.pendingMeta ? { meta: this.pendingMeta } : {},
|
|
3982
4789
|
...Object.keys(this.flags).length > 0 ? { flags: this.flags } : {},
|
|
3983
4790
|
// This device's clock as the request leaves, on the same clock as the
|
|
@@ -3989,20 +4796,24 @@ var Transport = class {
|
|
|
3989
4796
|
sentAt: Date.now()
|
|
3990
4797
|
});
|
|
3991
4798
|
}
|
|
3992
|
-
/**
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
|
|
3998
|
-
|
|
3999
|
-
|
|
4799
|
+
/**
|
|
4800
|
+
* The body, assembled from the events' cached JSON rather than by
|
|
4801
|
+
* stringifying them again: a retry resends exactly the bytes the first
|
|
4802
|
+
* attempt sent, which is also what lets ingest read it as a retry.
|
|
4803
|
+
*/
|
|
4804
|
+
body(chunk) {
|
|
4805
|
+
return `{"events":[${chunk.parts.join(",")}],${this.envelope(chunk.seq).slice(1)}`;
|
|
4806
|
+
}
|
|
4807
|
+
/** One request, and what to do next. */
|
|
4808
|
+
async send(chunk) {
|
|
4809
|
+
var _a, _b, _c, _d, _e, _f;
|
|
4810
|
+
const retry = chunk.attempts > 0;
|
|
4811
|
+
chunk.attempts += 1;
|
|
4812
|
+
(_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, chunk.seq + 1);
|
|
4813
|
+
const payload = this.body(chunk);
|
|
4814
|
+
let response;
|
|
4000
4815
|
try {
|
|
4001
|
-
|
|
4002
|
-
this.lastAttemptedSeq = this.seq;
|
|
4003
|
-
(_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, this.seq + 1);
|
|
4004
|
-
const payload = this.body(events);
|
|
4005
|
-
const response = await fetch(`${this.options.ingestUrl}/v1/ingest/events`, {
|
|
4816
|
+
response = await fetch(`${this.options.ingestUrl}/v1/ingest/events`, {
|
|
4006
4817
|
method: "POST",
|
|
4007
4818
|
headers: { "Content-Type": "application/json" },
|
|
4008
4819
|
body: payload,
|
|
@@ -4011,84 +4822,215 @@ var Transport = class {
|
|
|
4011
4822
|
credentials: "omit",
|
|
4012
4823
|
keepalive: withinKeepaliveLimit(payload)
|
|
4013
4824
|
});
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
this.seq
|
|
4025
|
-
|
|
4026
|
-
this.consecutiveFailures = 0;
|
|
4027
|
-
(_d = (_c = this.hooks).onSent) == null ? void 0 : _d.call(_c, this.seq - 1, events.length);
|
|
4028
|
-
return;
|
|
4825
|
+
} catch (e) {
|
|
4826
|
+
return this.registerFailure(void 0);
|
|
4827
|
+
}
|
|
4828
|
+
if (this.state === "stopped" || this.pending !== chunk) return "stopped";
|
|
4829
|
+
if (response.ok) {
|
|
4830
|
+
const nextSeq = retry ? void 0 : await staleSeqHint(response, chunk.seq);
|
|
4831
|
+
if (nextSeq !== void 0 && this.staleRecoveries < MAX_STALE_RECOVERIES) {
|
|
4832
|
+
this.staleRecoveries += 1;
|
|
4833
|
+
chunk.seq = nextSeq;
|
|
4834
|
+
chunk.attempts = 0;
|
|
4835
|
+
this.seq = Math.max(this.seq, nextSeq + 1);
|
|
4836
|
+
return "again";
|
|
4029
4837
|
}
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4838
|
+
this.pending = void 0;
|
|
4839
|
+
this.staleRecoveries = 0;
|
|
4840
|
+
this.pendingMeta = void 0;
|
|
4841
|
+
this.consecutiveFailures = 0;
|
|
4842
|
+
this.consecutiveRefusals = 0;
|
|
4843
|
+
this.backoffUntil = 0;
|
|
4844
|
+
(_d = (_c = this.hooks).onSent) == null ? void 0 : _d.call(_c, chunk.seq, chunk.events.length);
|
|
4845
|
+
return "sent";
|
|
4846
|
+
}
|
|
4847
|
+
const status = response.status;
|
|
4848
|
+
if (status === 413) return this.shrink(chunk, utf8Length(payload));
|
|
4849
|
+
if (status === 400 || status === 422) {
|
|
4850
|
+
this.pending = void 0;
|
|
4851
|
+
this.consecutiveRefusals += 1;
|
|
4852
|
+
if (this.consecutiveRefusals >= MAX_CONSECUTIVE_REFUSALS) {
|
|
4853
|
+
this.stop(`rejected_${status}`);
|
|
4854
|
+
return "stopped";
|
|
4033
4855
|
}
|
|
4034
|
-
this.
|
|
4035
|
-
this.
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
this.inFlight = false;
|
|
4041
|
-
if (resend) void this.flush();
|
|
4856
|
+
(_f = (_e = this.hooks).onWarning) == null ? void 0 : _f.call(_e, `ingest refused a chunk (${status}); its ${chunk.events.length} events are dropped`);
|
|
4857
|
+
this.afterGap();
|
|
4858
|
+
return "again";
|
|
4859
|
+
}
|
|
4860
|
+
if (status === 408 || status === 429 || status >= 500) {
|
|
4861
|
+
return this.registerFailure(retryAfterMs(response));
|
|
4042
4862
|
}
|
|
4863
|
+
this.stop(`rejected_${status}`);
|
|
4864
|
+
return "stopped";
|
|
4043
4865
|
}
|
|
4044
4866
|
/**
|
|
4045
|
-
*
|
|
4046
|
-
*
|
|
4047
|
-
*
|
|
4048
|
-
*
|
|
4049
|
-
*
|
|
4867
|
+
* `413`: this ingest takes less than we thought. The chunk was not stored,
|
|
4868
|
+
* so its events go back to the front of the queue and are re-cut under a
|
|
4869
|
+
* lower limit; its number is not reused, so no number ever carries two
|
|
4870
|
+
* different sets of events. A chunk of one event that is still too large is
|
|
4871
|
+
* dropped by `nextChunk` on the way back out.
|
|
4050
4872
|
*/
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
this.
|
|
4873
|
+
shrink(chunk, bytes) {
|
|
4874
|
+
var _a, _b;
|
|
4875
|
+
this.pending = void 0;
|
|
4876
|
+
if (this.bodyLimit <= MIN_BODY_BYTES) {
|
|
4877
|
+
this.stop("rejected_413");
|
|
4878
|
+
return "stopped";
|
|
4879
|
+
}
|
|
4880
|
+
this.buffer = [...chunk.events, ...this.buffer];
|
|
4881
|
+
this.bodyLimit = Math.max(MIN_BODY_BYTES, Math.min(this.bodyLimit, Math.floor(bytes / 2)));
|
|
4882
|
+
this.targetBytes = Math.min(this.targetBytes, this.bodyLimit);
|
|
4883
|
+
(_b = (_a = this.hooks).onWarning) == null ? void 0 : _b.call(_a, `ingest refused ${Math.ceil(bytes / 1024)} KB as too large; sending at most ${Math.floor(this.bodyLimit / 1024)} KB per chunk`);
|
|
4884
|
+
return "again";
|
|
4054
4885
|
}
|
|
4055
|
-
|
|
4886
|
+
/**
|
|
4887
|
+
* Counts a failed delivery and schedules the next attempt: 2 s, 4 s, 8 s …
|
|
4888
|
+
* up to a minute, each with jitter so a thousand tabs that lost the same
|
|
4889
|
+
* Wi-Fi do not all come back in the same second. The chunk stays pending,
|
|
4890
|
+
* to be resent under the same number.
|
|
4891
|
+
*
|
|
4892
|
+
* Recording does not stop over this unless the page asked for it with
|
|
4893
|
+
* `maxConsecutiveFailures`: the buffer exists for exactly this, and a
|
|
4894
|
+
* phone in a tunnel for half a minute must not end the visit's recording.
|
|
4895
|
+
*/
|
|
4896
|
+
registerFailure(serverWaitMs) {
|
|
4056
4897
|
this.consecutiveFailures += 1;
|
|
4057
|
-
|
|
4898
|
+
const limit = this.options.maxConsecutiveFailures;
|
|
4899
|
+
if (limit > 0 && this.consecutiveFailures >= limit) {
|
|
4058
4900
|
this.stop("too_many_failures");
|
|
4901
|
+
return "stopped";
|
|
4902
|
+
}
|
|
4903
|
+
const now = Date.now();
|
|
4904
|
+
const ceiling = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.min(this.consecutiveFailures - 1, 16));
|
|
4905
|
+
const delay = ceiling / 2 + Math.random() * (ceiling / 2);
|
|
4906
|
+
this.backoffUntil = now + delay;
|
|
4907
|
+
if (serverWaitMs !== void 0) this.serverWaitUntil = now + serverWaitMs;
|
|
4908
|
+
this.scheduleRetry(Math.max(this.backoffUntil, this.serverWaitUntil) - now);
|
|
4909
|
+
return "wait";
|
|
4910
|
+
}
|
|
4911
|
+
/**
|
|
4912
|
+
* The attempt at the end of a backoff. It skips the backoff check itself:
|
|
4913
|
+
* a timer may fire a fraction of a millisecond "early" against `Date.now`,
|
|
4914
|
+
* and refusing then would leave nothing scheduled until the next tick.
|
|
4915
|
+
*/
|
|
4916
|
+
scheduleRetry(delayMs) {
|
|
4917
|
+
var _a, _b;
|
|
4918
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
4919
|
+
this.retryTimer = setTimeout(() => {
|
|
4920
|
+
this.retryTimer = void 0;
|
|
4921
|
+
void this.flush(true);
|
|
4922
|
+
}, Math.max(0, Math.ceil(delayMs)));
|
|
4923
|
+
(_b = (_a = this.retryTimer).unref) == null ? void 0 : _b.call(_a);
|
|
4924
|
+
}
|
|
4925
|
+
/**
|
|
4926
|
+
* Keeps the buffer bounded during a long outage. Older events go first — a
|
|
4927
|
+
* replay missing a stretch is still useful; a crashed page is not. A chunk
|
|
4928
|
+
* waiting to be retried is the oldest of all, and goes whole: sending part
|
|
4929
|
+
* of it under its number could clash with an attempt that did arrive.
|
|
4930
|
+
*/
|
|
4931
|
+
dropOldest(count) {
|
|
4932
|
+
var _a, _b;
|
|
4933
|
+
let remaining = count;
|
|
4934
|
+
if (this.pending && !this.inFlight) {
|
|
4935
|
+
remaining -= this.pending.events.length;
|
|
4936
|
+
this.pending = void 0;
|
|
4937
|
+
}
|
|
4938
|
+
if (remaining > 0) this.buffer.splice(0, remaining);
|
|
4939
|
+
(_b = (_a = this.hooks).onWarning) == null ? void 0 : _b.call(_a, `dropped ${count} buffered events while ingest could not be reached`);
|
|
4940
|
+
this.afterGap();
|
|
4941
|
+
}
|
|
4942
|
+
/**
|
|
4943
|
+
* After events were dropped: DOM changes left before the next full snapshot
|
|
4944
|
+
* may describe nodes whose creation was dropped, and a replay applying them
|
|
4945
|
+
* breaks. They go, and when the buffer holds no snapshot to start again
|
|
4946
|
+
* from, the recorder is asked for one. Custom events, errors and the like
|
|
4947
|
+
* do not depend on the DOM and stay.
|
|
4948
|
+
*/
|
|
4949
|
+
afterGap() {
|
|
4950
|
+
const snapshot2 = this.buffer.findIndex((e) => e.type === EVENT_FULL_SNAPSHOT2);
|
|
4951
|
+
const end = snapshot2 === -1 ? this.buffer.length : snapshot2;
|
|
4952
|
+
this.buffer = [
|
|
4953
|
+
...this.buffer.slice(0, end).filter((e) => e.type !== EVENT_INCREMENTAL2),
|
|
4954
|
+
...this.buffer.slice(end)
|
|
4955
|
+
];
|
|
4956
|
+
if (snapshot2 === -1 && !this.awaitingSnapshot && this.hooks.onResyncNeeded) {
|
|
4957
|
+
this.awaitingSnapshot = true;
|
|
4958
|
+
this.hooks.onResyncNeeded();
|
|
4059
4959
|
}
|
|
4060
4960
|
}
|
|
4061
4961
|
/**
|
|
4062
|
-
* Last-gasp send when the page is going away.
|
|
4962
|
+
* Last-gasp send when the page is going away or hidden.
|
|
4063
4963
|
*
|
|
4064
4964
|
* `fetch` is cancelled once the document unloads, so the tail of every
|
|
4065
4965
|
* session would be lost. `sendBeacon` hands the payload to the browser, which
|
|
4066
|
-
* delivers it after the page is gone
|
|
4966
|
+
* delivers it after the page is gone — but only up to 64 KB in flight, so
|
|
4967
|
+
* the tail goes as a run of small chunks, oldest first, until the browser
|
|
4968
|
+
* says it has no room. What it refuses stays buffered for the page's next
|
|
4969
|
+
* flush, should there be one (a hidden tab that comes back, a page restored
|
|
4970
|
+
* from the back/forward cache).
|
|
4971
|
+
*
|
|
4972
|
+
* For a tab that is only hidden, and so likely to come back, nothing goes
|
|
4973
|
+
* while deliveries are failing or the server asked us to wait: a beacon
|
|
4974
|
+
* "queued" while offline is usually a beacon lost, where the buffer and the
|
|
4975
|
+
* backoff would have delivered it. On `pagehide` (`final`), the last
|
|
4976
|
+
* chance there is, it goes regardless.
|
|
4067
4977
|
*/
|
|
4068
|
-
flushWithBeacon() {
|
|
4069
|
-
|
|
4070
|
-
if (this.
|
|
4071
|
-
|
|
4072
|
-
const
|
|
4073
|
-
|
|
4978
|
+
flushWithBeacon(final = false) {
|
|
4979
|
+
if (this.state === "stopped" || !this.pending && this.buffer.length === 0) return false;
|
|
4980
|
+
if (!final && (this.consecutiveFailures > 0 || Date.now() < this.serverWaitUntil)) return false;
|
|
4981
|
+
if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
|
|
4982
|
+
const url = `${this.options.ingestUrl}/v1/ingest/events`;
|
|
4983
|
+
let queuedAny = false;
|
|
4984
|
+
let tooLarge = false;
|
|
4074
4985
|
try {
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
const
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
this.
|
|
4082
|
-
|
|
4986
|
+
const pending = this.pending;
|
|
4987
|
+
if (pending) {
|
|
4988
|
+
const payload = this.body(pending);
|
|
4989
|
+
if (!withinKeepaliveLimit(payload)) {
|
|
4990
|
+
tooLarge = true;
|
|
4991
|
+
} else {
|
|
4992
|
+
if (!this.beacon(url, pending, payload)) return queuedAny;
|
|
4993
|
+
queuedAny = true;
|
|
4994
|
+
if (!this.inFlight) {
|
|
4995
|
+
this.pending = void 0;
|
|
4996
|
+
this.pendingMeta = void 0;
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
}
|
|
5000
|
+
while (!tooLarge && this.buffer.length > 0) {
|
|
5001
|
+
const chunk = this.nextChunk(KEEPALIVE_BODY_LIMIT_BYTES, KEEPALIVE_BODY_LIMIT_BYTES, true);
|
|
5002
|
+
if (!chunk) {
|
|
5003
|
+
tooLarge = true;
|
|
5004
|
+
break;
|
|
5005
|
+
}
|
|
5006
|
+
const payload = this.body(chunk);
|
|
5007
|
+
if (!this.beacon(url, chunk, payload)) {
|
|
5008
|
+
this.buffer = [...chunk.events, ...this.buffer];
|
|
5009
|
+
this.seq = chunk.seq;
|
|
5010
|
+
break;
|
|
4083
5011
|
}
|
|
5012
|
+
queuedAny = true;
|
|
5013
|
+
this.pendingMeta = void 0;
|
|
4084
5014
|
}
|
|
4085
5015
|
} catch (e) {
|
|
4086
5016
|
}
|
|
4087
|
-
this.
|
|
4088
|
-
return
|
|
5017
|
+
if (tooLarge) void this.flush(true);
|
|
5018
|
+
return queuedAny;
|
|
5019
|
+
}
|
|
5020
|
+
/** Hands one chunk to the browser. False when its beacon queue is full. */
|
|
5021
|
+
beacon(url, chunk, payload) {
|
|
5022
|
+
var _a, _b;
|
|
5023
|
+
(_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, chunk.seq + 1);
|
|
5024
|
+
const blob = new Blob([payload], { type: "text/plain;charset=UTF-8" });
|
|
5025
|
+
return navigator.sendBeacon(url, blob);
|
|
4089
5026
|
}
|
|
4090
5027
|
};
|
|
4091
5028
|
|
|
5029
|
+
// src/version.ts
|
|
5030
|
+
var SDK_NAME = "@anyreplay/browser";
|
|
5031
|
+
var SDK_VERSION = true ? "0.2.1" : "0.0.0-dev";
|
|
5032
|
+
var PLATFORM = "web";
|
|
5033
|
+
|
|
4092
5034
|
// src/visitor.ts
|
|
4093
5035
|
var VISITOR_KEY = "anyreplay.vid";
|
|
4094
5036
|
var SESSION_KEY = "anyreplay.sid";
|
|
@@ -4193,12 +5135,14 @@ function forgetVisitor(local, session) {
|
|
|
4193
5135
|
}
|
|
4194
5136
|
|
|
4195
5137
|
// src/recorder.ts
|
|
5138
|
+
var RRWEB_DOM_CONTENT_LOADED = 0;
|
|
5139
|
+
var RRWEB_LOAD = 1;
|
|
4196
5140
|
var RAGE_CLICK_COUNT = 3;
|
|
4197
5141
|
var RAGE_CLICK_WINDOW_MS = 1e3;
|
|
4198
5142
|
var RAGE_CLICK_RADIUS_PX = 30;
|
|
4199
5143
|
var MAX_PENDING_EVENTS = 32;
|
|
4200
5144
|
var EDITABLE_SELECTOR = '[contenteditable]:not([contenteditable="false"])';
|
|
4201
|
-
function createRecorder(rawOptions) {
|
|
5145
|
+
function createRecorder(rawOptions, shell) {
|
|
4202
5146
|
const options = resolveOptions(rawOptions);
|
|
4203
5147
|
const local = safeStorage(() => globalThis.localStorage);
|
|
4204
5148
|
const session = safeStorage(() => globalThis.sessionStorage);
|
|
@@ -4206,6 +5150,8 @@ function createRecorder(rawOptions) {
|
|
|
4206
5150
|
let identity;
|
|
4207
5151
|
let transport;
|
|
4208
5152
|
let stopRecording;
|
|
5153
|
+
let diagnostics;
|
|
5154
|
+
let assets;
|
|
4209
5155
|
let pageCount = 0;
|
|
4210
5156
|
let pendingTraits;
|
|
4211
5157
|
const sendIdentity = (who, traits) => {
|
|
@@ -4213,22 +5159,34 @@ function createRecorder(rawOptions) {
|
|
|
4213
5159
|
method: "POST",
|
|
4214
5160
|
headers: { "Content-Type": "application/json" },
|
|
4215
5161
|
credentials: "omit",
|
|
4216
|
-
body: JSON.stringify({
|
|
5162
|
+
body: JSON.stringify({
|
|
5163
|
+
projectKey: options.projectKey,
|
|
5164
|
+
sessionId: who.sessionId,
|
|
5165
|
+
...traits,
|
|
5166
|
+
...options.appId ? { appId: options.appId } : {}
|
|
5167
|
+
})
|
|
4217
5168
|
}).catch(() => {
|
|
4218
5169
|
});
|
|
4219
5170
|
};
|
|
4220
|
-
const collectMeta = (who) =>
|
|
4221
|
-
|
|
4222
|
-
|
|
4223
|
-
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4230
|
-
|
|
4231
|
-
|
|
5171
|
+
const collectMeta = (who) => {
|
|
5172
|
+
var _a, _b;
|
|
5173
|
+
return {
|
|
5174
|
+
startedAt: Date.now(),
|
|
5175
|
+
url: safeLocation(),
|
|
5176
|
+
lang: typeof navigator !== "undefined" ? navigator.language : void 0,
|
|
5177
|
+
userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
|
|
5178
|
+
screenWidth: typeof window !== "undefined" ? window.innerWidth : void 0,
|
|
5179
|
+
screenHeight: typeof window !== "undefined" ? window.innerHeight : void 0,
|
|
5180
|
+
referrer: typeof document !== "undefined" ? document.referrer || void 0 : void 0,
|
|
5181
|
+
platform: (_a = shell == null ? void 0 : shell.platform) != null ? _a : PLATFORM,
|
|
5182
|
+
sdk: (_b = shell == null ? void 0 : shell.sdk) != null ? _b : { name: SDK_NAME, version: SDK_VERSION },
|
|
5183
|
+
...(shell == null ? void 0 : shell.appVersion) ? { appVersion: shell.appVersion.slice(0, 40) } : {},
|
|
5184
|
+
...(shell == null ? void 0 : shell.deviceModel) ? { deviceModel: shell.deviceModel.slice(0, 64) } : {},
|
|
5185
|
+
// The cap is a decision about starting a session, so it goes only where
|
|
5186
|
+
// ingest makes that decision: chunk 0. A resumed page starts later.
|
|
5187
|
+
...options.maxSessionsPerMonth !== void 0 && who.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
|
|
5188
|
+
};
|
|
5189
|
+
};
|
|
4232
5190
|
const recentClicks = [];
|
|
4233
5191
|
const noteInteraction = (event, channel) => {
|
|
4234
5192
|
var _a;
|
|
@@ -4256,6 +5214,7 @@ function createRecorder(rawOptions) {
|
|
|
4256
5214
|
const next = pending[0];
|
|
4257
5215
|
record.addCustomEvent(next.tag, next.payload);
|
|
4258
5216
|
pending.shift();
|
|
5217
|
+
if (next.tag === ERROR_TAG) transport == null ? void 0 : transport.mergeFlags({ hasError: true });
|
|
4259
5218
|
}
|
|
4260
5219
|
} catch (e) {
|
|
4261
5220
|
if (!drainScheduled && status === "recording") {
|
|
@@ -4322,17 +5281,52 @@ function createRecorder(rawOptions) {
|
|
|
4322
5281
|
status = "stopped";
|
|
4323
5282
|
stopRecording == null ? void 0 : stopRecording();
|
|
4324
5283
|
stopRecording = void 0;
|
|
5284
|
+
diagnostics == null ? void 0 : diagnostics.stop();
|
|
5285
|
+
diagnostics = void 0;
|
|
5286
|
+
assets == null ? void 0 : assets.stop();
|
|
5287
|
+
assets = void 0;
|
|
4325
5288
|
if (reason === "rejected_402") startCooldown(session);
|
|
4326
5289
|
if (options.debug) console.warn(`[anyreplay] recording stopped: ${reason}`);
|
|
4327
|
-
}
|
|
5290
|
+
},
|
|
5291
|
+
onWarning: (message) => {
|
|
5292
|
+
if (options.debug) console.warn(`[anyreplay] ${message}`);
|
|
5293
|
+
},
|
|
5294
|
+
// Events were dropped (a long outage, or one too large to send), so
|
|
5295
|
+
// the DOM changes after the gap describe nodes ingest never got. A
|
|
5296
|
+
// fresh full snapshot starts the page over. Taken on the next turn,
|
|
5297
|
+
// not here: this can be called from inside rrweb's own emit, in the
|
|
5298
|
+
// middle of handing over a batch of mutations.
|
|
5299
|
+
onResyncNeeded: () => setTimeout(() => takeSnapshot(2), 0)
|
|
4328
5300
|
}
|
|
4329
5301
|
);
|
|
4330
5302
|
transport = channel;
|
|
4331
5303
|
status = "recording";
|
|
5304
|
+
const takeSnapshot = (triesLeft) => {
|
|
5305
|
+
if (status !== "recording" || !stopRecording) return;
|
|
5306
|
+
try {
|
|
5307
|
+
record.takeFullSnapshot(true);
|
|
5308
|
+
} catch (e) {
|
|
5309
|
+
if (triesLeft > 0) setTimeout(() => takeSnapshot(triesLeft - 1), 1e3);
|
|
5310
|
+
}
|
|
5311
|
+
};
|
|
4332
5312
|
lastUrl = safeLocation();
|
|
4333
5313
|
channel.setMeta(collectMeta(who));
|
|
4334
5314
|
channel.mergeFlags({ pageCount });
|
|
4335
5315
|
channel.start();
|
|
5316
|
+
const prefix = options.assets === "inline" ? localAssetPrefix() : null;
|
|
5317
|
+
if (prefix && typeof fetch === "function") {
|
|
5318
|
+
assets = new AssetCollector({
|
|
5319
|
+
prefix,
|
|
5320
|
+
ingestUrl: options.ingestUrl,
|
|
5321
|
+
projectKey: options.projectKey,
|
|
5322
|
+
appId: options.appId,
|
|
5323
|
+
// Bound now, before the network diagnostics wrap `fetch`: reading the
|
|
5324
|
+
// app's own files is the recorder's business, not the app's traffic.
|
|
5325
|
+
fetch: fetch.bind(globalThis),
|
|
5326
|
+
emit: (payload) => emitCustom(ASSET_TAG, payload),
|
|
5327
|
+
debug: options.debug
|
|
5328
|
+
});
|
|
5329
|
+
}
|
|
4336
5330
|
const policy = {
|
|
4337
5331
|
maskEveryField: options.maskAllInputs || options.maskAllTyping,
|
|
4338
5332
|
maskClass: options.maskClass
|
|
@@ -4340,7 +5334,9 @@ function createRecorder(rawOptions) {
|
|
|
4340
5334
|
stopRecording = record({
|
|
4341
5335
|
emit: (event) => {
|
|
4342
5336
|
const typed = event;
|
|
5337
|
+
if (typed.type === RRWEB_DOM_CONTENT_LOADED || typed.type === RRWEB_LOAD) return;
|
|
4343
5338
|
scrubEvent(typed, (id) => record.mirror.getNode(id), policy);
|
|
5339
|
+
assets == null ? void 0 : assets.scan(typed);
|
|
4344
5340
|
noteInteraction(typed, channel);
|
|
4345
5341
|
touchSession(session, typed.timestamp);
|
|
4346
5342
|
channel.push(typed);
|
|
@@ -4350,6 +5346,9 @@ function createRecorder(rawOptions) {
|
|
|
4350
5346
|
maskTextClass: options.maskClass,
|
|
4351
5347
|
maskTextSelector: options.maskAllTyping ? EDITABLE_SELECTOR : void 0,
|
|
4352
5348
|
blockClass: options.blockClass,
|
|
5349
|
+
// Readable stylesheets go into the snapshot as text, in both asset
|
|
5350
|
+
// modes. rrweb's default, written out because 'inline' depends on it.
|
|
5351
|
+
inlineStylesheet: true,
|
|
4353
5352
|
// Canvas capture is expensive and off unless a customer opts in later.
|
|
4354
5353
|
recordCanvas: false,
|
|
4355
5354
|
collectFonts: false
|
|
@@ -4364,13 +5363,21 @@ function createRecorder(rawOptions) {
|
|
|
4364
5363
|
const attachLifecycleHooks = (channel) => {
|
|
4365
5364
|
if (typeof window === "undefined") return;
|
|
4366
5365
|
window.addEventListener("pagehide", () => {
|
|
4367
|
-
channel.flushWithBeacon();
|
|
5366
|
+
channel.flushWithBeacon(true);
|
|
4368
5367
|
}, { capture: true });
|
|
4369
5368
|
document.addEventListener("visibilitychange", () => {
|
|
4370
5369
|
if (document.visibilityState === "hidden") channel.flushWithBeacon();
|
|
5370
|
+
else channel.resume();
|
|
5371
|
+
});
|
|
5372
|
+
window.addEventListener("online", () => channel.resume());
|
|
5373
|
+
diagnostics = startDiagnostics({
|
|
5374
|
+
errors: options.recordErrors,
|
|
5375
|
+
console: options.recordConsole,
|
|
5376
|
+
network: options.recordNetwork,
|
|
5377
|
+
ingestUrl: options.ingestUrl,
|
|
5378
|
+
emit: emitCustom,
|
|
5379
|
+
onError: () => channel.mergeFlags({ hasError: true })
|
|
4371
5380
|
});
|
|
4372
|
-
window.addEventListener("error", () => channel.mergeFlags({ hasError: true }));
|
|
4373
|
-
window.addEventListener("unhandledrejection", () => channel.mergeFlags({ hasError: true }));
|
|
4374
5381
|
const patch2 = (method) => {
|
|
4375
5382
|
const original = history[method].bind(history);
|
|
4376
5383
|
history[method] = function patched(...args) {
|
|
@@ -4400,8 +5407,12 @@ function createRecorder(rawOptions) {
|
|
|
4400
5407
|
return;
|
|
4401
5408
|
}
|
|
4402
5409
|
transport == null ? void 0 : transport.stop("consent_withdrawn");
|
|
4403
|
-
if (status === "idle" || status === "awaiting-consent"
|
|
5410
|
+
if (status === "idle" || status === "awaiting-consent" || status === "sampled-out") {
|
|
5411
|
+
status = "awaiting-consent";
|
|
5412
|
+
identity = void 0;
|
|
5413
|
+
}
|
|
4404
5414
|
pendingTraits = void 0;
|
|
5415
|
+
pending.length = 0;
|
|
4405
5416
|
forgetVisitor(local, session);
|
|
4406
5417
|
},
|
|
4407
5418
|
identify: (traits) => {
|
|
@@ -4421,14 +5432,24 @@ function createRecorder(rawOptions) {
|
|
|
4421
5432
|
}
|
|
4422
5433
|
emitCustom(TRACK_TAG, payload);
|
|
4423
5434
|
},
|
|
5435
|
+
trackError: (error) => {
|
|
5436
|
+
if (!options.recordErrors) return;
|
|
5437
|
+
if (diagnostics) {
|
|
5438
|
+
diagnostics.error(error);
|
|
5439
|
+
return;
|
|
5440
|
+
}
|
|
5441
|
+
emitCustom(ERROR_TAG, errorPayload("error", error));
|
|
5442
|
+
if (status === "recording") transport == null ? void 0 : transport.mergeFlags({ hasError: true });
|
|
5443
|
+
},
|
|
4424
5444
|
stop: () => {
|
|
4425
5445
|
transport == null ? void 0 : transport.stop("stopped_by_page");
|
|
4426
5446
|
if (status === "idle" || status === "awaiting-consent") status = "stopped";
|
|
4427
5447
|
pendingTraits = void 0;
|
|
4428
5448
|
},
|
|
5449
|
+
// Asked for by the page, so it skips our own backoff (never a server's Retry-After).
|
|
4429
5450
|
flush: () => {
|
|
4430
5451
|
var _a;
|
|
4431
|
-
return (_a = transport == null ? void 0 : transport.flush()) != null ? _a : Promise.resolve();
|
|
5452
|
+
return (_a = transport == null ? void 0 : transport.flush(true)) != null ? _a : Promise.resolve();
|
|
4432
5453
|
}
|
|
4433
5454
|
};
|
|
4434
5455
|
}
|
|
@@ -4440,20 +5461,36 @@ function safeLocation() {
|
|
|
4440
5461
|
}
|
|
4441
5462
|
}
|
|
4442
5463
|
export {
|
|
5464
|
+
ASSET_LIMITS,
|
|
5465
|
+
ASSET_TAG,
|
|
5466
|
+
CONSOLE_TAG,
|
|
4443
5467
|
ConfigError,
|
|
4444
5468
|
DEFAULTS,
|
|
5469
|
+
ERROR_TAG,
|
|
4445
5470
|
EVENT_NAME_PATTERN,
|
|
5471
|
+
LIMITS,
|
|
4446
5472
|
MAX_PROPERTIES_BYTES,
|
|
4447
5473
|
NAVIGATE_TAG,
|
|
5474
|
+
NETWORK_TAG,
|
|
5475
|
+
REDACTED,
|
|
5476
|
+
SDK_NAME,
|
|
5477
|
+
SDK_VERSION,
|
|
4448
5478
|
TRACK_TAG,
|
|
4449
5479
|
Transport,
|
|
5480
|
+
consolePayload,
|
|
4450
5481
|
createRecorder,
|
|
5482
|
+
defaultAssetMode,
|
|
5483
|
+
errorPayload,
|
|
4451
5484
|
forgetVisitor,
|
|
4452
5485
|
hashToUnitInterval,
|
|
4453
5486
|
isSampledIn,
|
|
5487
|
+
localAssetPrefix,
|
|
5488
|
+
redactText,
|
|
5489
|
+
redactUrl,
|
|
4454
5490
|
resolveIdentity,
|
|
4455
5491
|
resolveOptions,
|
|
4456
5492
|
safeStorage,
|
|
5493
|
+
startDiagnostics,
|
|
4457
5494
|
uuidv4,
|
|
4458
5495
|
validateTrack
|
|
4459
5496
|
};
|