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