@anyreplay/browser 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,323 @@ 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
+ const body = await known.json();
3869
+ if (Array.isArray(body.known) && body.known.includes(sha256)) return true;
3870
+ }
3871
+ const upload = await post("/v1/ingest/assets", { projectKey, sha256, data: toBase64(bytes), ...app });
3872
+ return upload.ok;
3873
+ }
3874
+ skip(url, why) {
3875
+ if (this.deps.debug) console.warn(`[anyreplay] not included in the replay (${why}): ${url}`);
3876
+ return null;
3877
+ }
3878
+ };
3879
+ function toBase64(bytes) {
3880
+ let binary = "";
3881
+ for (let i = 0; i < bytes.length; i += 32768) {
3882
+ binary += String.fromCharCode.apply(null, bytes.subarray(i, i + 32768));
3883
+ }
3884
+ return btoa(binary);
3885
+ }
3886
+ async function sha256Hex(bytes) {
3887
+ try {
3888
+ const subtle = typeof crypto !== "undefined" ? crypto.subtle : void 0;
3889
+ if (subtle) {
3890
+ const digest = new Uint8Array(await subtle.digest("SHA-256", bytes));
3891
+ return Array.from(digest, (b) => b.toString(16).padStart(2, "0")).join("");
3892
+ }
3893
+ } catch (e) {
3894
+ }
3895
+ return sha256HexSync(bytes);
3896
+ }
3897
+ var K = [
3898
+ 1116352408,
3899
+ 1899447441,
3900
+ 3049323471,
3901
+ 3921009573,
3902
+ 961987163,
3903
+ 1508970993,
3904
+ 2453635748,
3905
+ 2870763221,
3906
+ 3624381080,
3907
+ 310598401,
3908
+ 607225278,
3909
+ 1426881987,
3910
+ 1925078388,
3911
+ 2162078206,
3912
+ 2614888103,
3913
+ 3248222580,
3914
+ 3835390401,
3915
+ 4022224774,
3916
+ 264347078,
3917
+ 604807628,
3918
+ 770255983,
3919
+ 1249150122,
3920
+ 1555081692,
3921
+ 1996064986,
3922
+ 2554220882,
3923
+ 2821834349,
3924
+ 2952996808,
3925
+ 3210313671,
3926
+ 3336571891,
3927
+ 3584528711,
3928
+ 113926993,
3929
+ 338241895,
3930
+ 666307205,
3931
+ 773529912,
3932
+ 1294757372,
3933
+ 1396182291,
3934
+ 1695183700,
3935
+ 1986661051,
3936
+ 2177026350,
3937
+ 2456956037,
3938
+ 2730485921,
3939
+ 2820302411,
3940
+ 3259730800,
3941
+ 3345764771,
3942
+ 3516065817,
3943
+ 3600352804,
3944
+ 4094571909,
3945
+ 275423344,
3946
+ 430227734,
3947
+ 506948616,
3948
+ 659060556,
3949
+ 883997877,
3950
+ 958139571,
3951
+ 1322822218,
3952
+ 1537002063,
3953
+ 1747873779,
3954
+ 1955562222,
3955
+ 2024104815,
3956
+ 2227730452,
3957
+ 2361852424,
3958
+ 2428436474,
3959
+ 2756734187,
3960
+ 3204031479,
3961
+ 3329325298
3962
+ ];
3963
+ function sha256HexSync(bytes) {
3964
+ const length = bytes.length;
3965
+ const padded = new Uint8Array(length + 9 + 63 >>> 6 << 6);
3966
+ padded.set(bytes);
3967
+ padded[length] = 128;
3968
+ const view = new DataView(padded.buffer);
3969
+ view.setUint32(padded.length - 8, Math.floor(length / 536870912));
3970
+ view.setUint32(padded.length - 4, length << 3 >>> 0);
3971
+ const h = [1779033703, 3144134277, 1013904242, 2773480762, 1359893119, 2600822924, 528734635, 1541459225];
3972
+ const w = new Array(64);
3973
+ const rotr = (x, n) => x >>> n | x << 32 - n;
3974
+ for (let offset = 0; offset < padded.length; offset += 64) {
3975
+ for (let i = 0; i < 16; i += 1) w[i] = view.getUint32(offset + i * 4);
3976
+ for (let i = 16; i < 64; i += 1) {
3977
+ const a2 = w[i - 15];
3978
+ const b2 = w[i - 2];
3979
+ 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;
3980
+ }
3981
+ let [a, b, c, d, e, f, g, hh] = h;
3982
+ for (let i = 0; i < 64; i += 1) {
3983
+ const t1 = hh + (rotr(e, 6) ^ rotr(e, 11) ^ rotr(e, 25)) + (e & f ^ ~e & g) + K[i] + w[i] | 0;
3984
+ const t2 = (rotr(a, 2) ^ rotr(a, 13) ^ rotr(a, 22)) + (a & b ^ a & c ^ b & c) | 0;
3985
+ hh = g;
3986
+ g = f;
3987
+ f = e;
3988
+ e = d + t1 | 0;
3989
+ d = c;
3990
+ c = b;
3991
+ b = a;
3992
+ a = t1 + t2 | 0;
3993
+ }
3994
+ h[0] = h[0] + a | 0;
3995
+ h[1] = h[1] + b | 0;
3996
+ h[2] = h[2] + c | 0;
3997
+ h[3] = h[3] + d | 0;
3998
+ h[4] = h[4] + e | 0;
3999
+ h[5] = h[5] + f | 0;
4000
+ h[6] = h[6] + g | 0;
4001
+ h[7] = h[7] + hh | 0;
4002
+ }
4003
+ return h.map((x) => (x >>> 0).toString(16).padStart(8, "0")).join("");
4004
+ }
4005
+
3673
4006
  // src/config.ts
3674
4007
  var DEFAULTS = {
3675
4008
  ingestUrl: "https://in.anyreplay.com",
@@ -3685,15 +4018,23 @@ var DEFAULTS = {
3685
4018
  requireConsent: false,
3686
4019
  maxEventsPerChunk: 200,
3687
4020
  flushIntervalMs: 5e3,
3688
- maxConsecutiveFailures: 5,
4021
+ // Never: an outage is what the buffer is for. Set a number to give up instead.
4022
+ maxConsecutiveFailures: 0,
4023
+ // The three diagnostic streams. On, because "why did it break" is the
4024
+ // question a replay is opened to answer; each is a documented switch, and
4025
+ // what they can and cannot contain is in diagnostics.ts.
4026
+ recordErrors: true,
4027
+ recordConsole: true,
4028
+ recordNetwork: true,
3689
4029
  debug: false
3690
4030
  };
3691
4031
  var ConfigError = class extends Error {
3692
4032
  };
3693
4033
  var KEY_PATTERN = /^ar_pk_(live|test)_[0-9a-f]{24}$/;
3694
4034
  var MAX_SESSION_CAP = 1e6;
4035
+ var APP_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{1,254}$/;
3695
4036
  function resolveOptions(input) {
3696
- var _a, _b, _c;
4037
+ var _a, _b, _c, _d;
3697
4038
  if (!input || typeof input !== "object") throw new ConfigError("anyreplay: options object required");
3698
4039
  if (!KEY_PATTERN.test((_a = input.projectKey) != null ? _a : "")) {
3699
4040
  throw new ConfigError("anyreplay: projectKey looks wrong \u2014 copy it from the dashboard");
@@ -3706,53 +4047,42 @@ function resolveOptions(input) {
3706
4047
  if (cap !== void 0 && !(Number.isInteger(cap) && cap >= 1 && cap <= MAX_SESSION_CAP)) {
3707
4048
  throw new ConfigError(`anyreplay: maxSessionsPerMonth must be a whole number between 1 and ${MAX_SESSION_CAP}`);
3708
4049
  }
4050
+ const appId = (_c = input.appId) == null ? void 0 : _c.trim();
4051
+ if (input.appId !== void 0 && !(appId && APP_ID_PATTERN.test(appId))) {
4052
+ throw new ConfigError("anyreplay: appId should be an app identifier such as com.example.app");
4053
+ }
4054
+ let assets = input.assets;
4055
+ if (assets !== void 0 && assets !== "reference" && assets !== "inline") {
4056
+ if (typeof console !== "undefined") {
4057
+ console.warn(`[anyreplay] assets ignored: expected 'reference' or 'inline', got ${JSON.stringify(assets)}.`);
4058
+ }
4059
+ assets = void 0;
4060
+ }
3709
4061
  return {
3710
4062
  ...DEFAULTS,
3711
4063
  ...input,
4064
+ ...appId ? { appId } : {},
4065
+ assets: assets != null ? assets : defaultAssetMode(),
3712
4066
  projectKey: input.projectKey,
3713
4067
  sampleRate,
3714
- ingestUrl: ((_c = input.ingestUrl) != null ? _c : DEFAULTS.ingestUrl).replace(/\/+$/, ""),
4068
+ ingestUrl: ((_d = input.ingestUrl) != null ? _d : DEFAULTS.ingestUrl).replace(/\/+$/, ""),
3715
4069
  maskAllInputs: readSwitch(input, "maskAllInputs"),
3716
- maskAllTyping: readSwitch(input, "maskAllTyping")
4070
+ maskAllTyping: readSwitch(input, "maskAllTyping"),
4071
+ recordErrors: readSwitch(input, "recordErrors"),
4072
+ recordConsole: readSwitch(input, "recordConsole"),
4073
+ recordNetwork: readSwitch(input, "recordNetwork")
3717
4074
  };
3718
4075
  }
3719
4076
  function readSwitch(input, name) {
3720
4077
  const value = input[name];
3721
4078
  if (typeof value === "boolean") return value;
3722
4079
  if (value !== void 0 && typeof console !== "undefined") {
3723
- console.warn(`[anyreplay] ${name} ignored: expected true or false, got ${typeof value}. Nothing is masked by this option.`);
4080
+ const consequence = name.startsWith("mask") ? "Nothing is masked by this option." : `${name.slice("record".length)} are recorded, as by default.`;
4081
+ console.warn(`[anyreplay] ${name} ignored: expected true or false, got ${typeof value}. ${consequence}`);
3724
4082
  }
3725
4083
  return DEFAULTS[name];
3726
4084
  }
3727
4085
 
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
4086
  // src/masking.ts
3757
4087
  var maskValue = (text) => "*".repeat(text.length);
3758
4088
  var MASK_EVERY_FIELD = {
@@ -3917,6 +4247,296 @@ function scrubEvent(event, getNode, policy) {
3917
4247
  }
3918
4248
  }
3919
4249
 
4250
+ // src/diagnostics.ts
4251
+ var ERROR_TAG = "anyreplay.error";
4252
+ var CONSOLE_TAG = "anyreplay.console";
4253
+ var NETWORK_TAG = "anyreplay.network";
4254
+ var LIMITS = { error: 100, console: 200, network: 500 };
4255
+ var MAX_MESSAGE = 1e3;
4256
+ var MAX_STACK = 4e3;
4257
+ var MAX_URL = 500;
4258
+ 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;
4259
+ var EMAIL = /[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g;
4260
+ var OPAQUE = /\b(?=[A-Za-z0-9_-]*\d)[A-Za-z0-9_-]{24,}\b/g;
4261
+ var DIGITS = /\b[0-9][0-9 -]{11,22}[0-9]\b/g;
4262
+ var REDACTED = "[redacted]";
4263
+ function redactText(text) {
4264
+ return text.replace(EMAIL, REDACTED).replace(DIGITS, (match) => looksLikeCardNumber(match.trim()) ? REDACTED : match).replace(OPAQUE, REDACTED);
4265
+ }
4266
+ function redactUrl(raw) {
4267
+ const text = raw.length > MAX_URL ? raw.slice(0, MAX_URL) : raw;
4268
+ let parsed;
4269
+ try {
4270
+ parsed = new URL(text, typeof location !== "undefined" ? location.href : "http://localhost");
4271
+ } catch (e) {
4272
+ return redactText(text);
4273
+ }
4274
+ for (const key of [...parsed.searchParams.keys()]) {
4275
+ if (SECRET_KEY.test(key)) parsed.searchParams.set(key, REDACTED);
4276
+ }
4277
+ parsed.hash = "";
4278
+ return redactText(`${parsed.origin}${parsed.pathname}${parsed.search}`);
4279
+ }
4280
+ var clip = (text, max) => text.length > max ? `${text.slice(0, max)}\u2026` : text;
4281
+ function describe(value) {
4282
+ if (typeof value === "string") return value;
4283
+ if (value === null) return "null";
4284
+ if (value === void 0) return "undefined";
4285
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
4286
+ if (value instanceof Error) return `${value.name}: ${value.message}`;
4287
+ try {
4288
+ const json = JSON.stringify(value);
4289
+ return json === void 0 ? Object.prototype.toString.call(value) : json;
4290
+ } catch (e) {
4291
+ return Object.prototype.toString.call(value);
4292
+ }
4293
+ }
4294
+ function errorPayload(kind, value, where) {
4295
+ const error = value instanceof Error ? value : null;
4296
+ const message = error ? `${error.name}: ${error.message}` : describe(value);
4297
+ const payload = { message: clip(redactText(message) || "error", MAX_MESSAGE), kind };
4298
+ if (error) {
4299
+ payload.name = error.name;
4300
+ if (typeof error.stack === "string" && error.stack.length > 0) {
4301
+ payload.stack = clip(redactText(error.stack), MAX_STACK);
4302
+ }
4303
+ }
4304
+ if (typeof (where == null ? void 0 : where.source) === "string" && where.source.length > 0) payload.source = redactUrl(where.source);
4305
+ if (typeof (where == null ? void 0 : where.line) === "number" && Number.isFinite(where.line)) payload.line = where.line;
4306
+ if (typeof (where == null ? void 0 : where.column) === "number" && Number.isFinite(where.column)) payload.column = where.column;
4307
+ return payload;
4308
+ }
4309
+ function consolePayload(level, args) {
4310
+ const joined = args.map(describe).join(" ");
4311
+ return { level, message: clip(redactText(joined), MAX_MESSAGE) };
4312
+ }
4313
+ function methodOf(value) {
4314
+ const text = typeof value === "string" ? value.toUpperCase() : "GET";
4315
+ return /^[A-Z]{3,10}$/.test(text) ? text : "GET";
4316
+ }
4317
+ function startDiagnostics(options) {
4318
+ const undo = [];
4319
+ const counts = { error: 0, console: 0, network: 0 };
4320
+ let flagged = false;
4321
+ const budget = (stream) => {
4322
+ if (counts[stream] >= LIMITS[stream]) return false;
4323
+ counts[stream] += 1;
4324
+ return true;
4325
+ };
4326
+ const flag = () => {
4327
+ if (flagged) return;
4328
+ flagged = true;
4329
+ try {
4330
+ options.onError();
4331
+ } catch (e) {
4332
+ }
4333
+ };
4334
+ const send = (tag, payload) => {
4335
+ try {
4336
+ options.emit(tag, payload);
4337
+ } catch (e) {
4338
+ }
4339
+ };
4340
+ let ingestOrigin = null;
4341
+ try {
4342
+ ingestOrigin = new URL(options.ingestUrl).origin;
4343
+ } catch (e) {
4344
+ }
4345
+ const ours = (url) => ingestOrigin !== null && url.startsWith(ingestOrigin);
4346
+ if (options.errors && typeof window !== "undefined") {
4347
+ const onError = (event) => {
4348
+ var _a;
4349
+ flag();
4350
+ if (!budget("error")) return;
4351
+ send(ERROR_TAG, errorPayload("error", (_a = event.error) != null ? _a : event.message, {
4352
+ source: event.filename,
4353
+ line: event.lineno,
4354
+ column: event.colno
4355
+ }));
4356
+ };
4357
+ const onRejection = (event) => {
4358
+ flag();
4359
+ if (!budget("error")) return;
4360
+ send(ERROR_TAG, errorPayload("rejection", event.reason));
4361
+ };
4362
+ window.addEventListener("error", onError, { capture: true });
4363
+ window.addEventListener("unhandledrejection", onRejection, { capture: true });
4364
+ undo.push(() => {
4365
+ window.removeEventListener("error", onError, { capture: true });
4366
+ window.removeEventListener("unhandledrejection", onRejection, { capture: true });
4367
+ });
4368
+ }
4369
+ if (options.console && typeof console !== "undefined") {
4370
+ for (const level of ["error", "warn"]) {
4371
+ const original = console[level];
4372
+ if (typeof original !== "function") continue;
4373
+ const patched = function patchedConsole(...args) {
4374
+ try {
4375
+ const first = args[0];
4376
+ const own = typeof first === "string" && first.startsWith("[anyreplay]");
4377
+ if (!own && budget("console")) send(CONSOLE_TAG, consolePayload(level, args));
4378
+ } catch (e) {
4379
+ }
4380
+ original.apply(this, args);
4381
+ };
4382
+ console[level] = patched;
4383
+ undo.push(() => {
4384
+ console[level] = original;
4385
+ });
4386
+ }
4387
+ }
4388
+ if (options.network && typeof window !== "undefined") {
4389
+ const originalFetch = window.fetch;
4390
+ if (typeof originalFetch === "function") {
4391
+ const patchedFetch = function patchedFetch2(input, init) {
4392
+ var _a, _b;
4393
+ let url = "";
4394
+ let method = "GET";
4395
+ try {
4396
+ url = typeof input === "string" ? input : input instanceof URL ? input.href : (_a = input.url) != null ? _a : "";
4397
+ method = methodOf((_b = init == null ? void 0 : init.method) != null ? _b : input == null ? void 0 : input.method);
4398
+ } catch (e) {
4399
+ }
4400
+ const started = Date.now();
4401
+ const result = originalFetch.call(window, input, init);
4402
+ if (!url || ours(url)) return result;
4403
+ return result.then(
4404
+ (response) => {
4405
+ try {
4406
+ if (budget("network")) {
4407
+ if (response.status >= 400) flag();
4408
+ send(NETWORK_TAG, {
4409
+ url: redactUrl(url),
4410
+ method,
4411
+ status: response.status,
4412
+ durationMs: Date.now() - started,
4413
+ initiator: "fetch"
4414
+ });
4415
+ }
4416
+ } catch (e) {
4417
+ }
4418
+ return response;
4419
+ },
4420
+ (reason) => {
4421
+ try {
4422
+ flag();
4423
+ if (budget("network")) {
4424
+ send(NETWORK_TAG, {
4425
+ url: redactUrl(url),
4426
+ method,
4427
+ status: 0,
4428
+ durationMs: Date.now() - started,
4429
+ initiator: "fetch",
4430
+ failed: true
4431
+ });
4432
+ }
4433
+ } catch (e) {
4434
+ }
4435
+ throw reason;
4436
+ }
4437
+ );
4438
+ };
4439
+ window.fetch = patchedFetch;
4440
+ undo.push(() => {
4441
+ window.fetch = originalFetch;
4442
+ });
4443
+ }
4444
+ const Xhr = window.XMLHttpRequest;
4445
+ if (typeof Xhr === "function") {
4446
+ const open = Xhr.prototype.open;
4447
+ const sendMethod = Xhr.prototype.send;
4448
+ const seen = /* @__PURE__ */ new WeakMap();
4449
+ Xhr.prototype.open = function patchedOpen(method, url, ...rest) {
4450
+ try {
4451
+ const href = typeof url === "string" ? url : url.href;
4452
+ if (href && !ours(href)) seen.set(this, { url: href, method: methodOf(method), started: 0 });
4453
+ } catch (e) {
4454
+ }
4455
+ return open.call(this, method, url, ...rest);
4456
+ };
4457
+ Xhr.prototype.send = function patchedSend(...args) {
4458
+ try {
4459
+ const request = seen.get(this);
4460
+ if (request) {
4461
+ request.started = Date.now();
4462
+ const done = (failed) => {
4463
+ try {
4464
+ if (failed || this.status >= 400) flag();
4465
+ if (!budget("network")) return;
4466
+ send(NETWORK_TAG, {
4467
+ url: redactUrl(request.url),
4468
+ method: request.method,
4469
+ status: this.status || 0,
4470
+ durationMs: Date.now() - request.started,
4471
+ initiator: "xhr",
4472
+ ...failed ? { failed: true } : {}
4473
+ });
4474
+ } catch (e) {
4475
+ }
4476
+ };
4477
+ this.addEventListener("load", () => done(false));
4478
+ this.addEventListener("error", () => done(true));
4479
+ this.addEventListener("timeout", () => done(true));
4480
+ this.addEventListener("abort", () => done(true));
4481
+ }
4482
+ } catch (e) {
4483
+ }
4484
+ return sendMethod.call(this, ...args);
4485
+ };
4486
+ undo.push(() => {
4487
+ Xhr.prototype.open = open;
4488
+ Xhr.prototype.send = sendMethod;
4489
+ });
4490
+ }
4491
+ }
4492
+ let stopped = false;
4493
+ return {
4494
+ error: (value) => {
4495
+ if (stopped) return;
4496
+ flag();
4497
+ if (budget("error")) send(ERROR_TAG, errorPayload("error", value));
4498
+ },
4499
+ stop: () => {
4500
+ if (stopped) return;
4501
+ stopped = true;
4502
+ for (let i = undo.length - 1; i >= 0; i -= 1) {
4503
+ try {
4504
+ undo[i]();
4505
+ } catch (e) {
4506
+ }
4507
+ }
4508
+ }
4509
+ };
4510
+ }
4511
+
4512
+ // src/events.ts
4513
+ var TRACK_TAG = "anyreplay.track";
4514
+ var NAVIGATE_TAG = "anyreplay.navigate";
4515
+ var EVENT_NAME_PATTERN = /^[A-Za-z0-9_.:-]{1,64}$/;
4516
+ var MAX_PROPERTIES_BYTES = 4 * 1024;
4517
+ function validateTrack(name, properties) {
4518
+ if (typeof name !== "string" || !EVENT_NAME_PATTERN.test(name)) {
4519
+ return `event name must be 1-64 characters of [A-Za-z0-9_.:-], got ${JSON.stringify(name)}`;
4520
+ }
4521
+ if (properties === void 0 || properties === null) return { name, properties: {} };
4522
+ if (typeof properties !== "object" || Array.isArray(properties)) {
4523
+ return `properties for "${name}" must be a plain object`;
4524
+ }
4525
+ let serialised;
4526
+ try {
4527
+ serialised = JSON.stringify(properties);
4528
+ } catch (e) {
4529
+ return `properties for "${name}" are not JSON-serialisable`;
4530
+ }
4531
+ if (serialised === void 0 || byteLength(serialised) > MAX_PROPERTIES_BYTES) {
4532
+ return `properties for "${name}" exceed ${MAX_PROPERTIES_BYTES} bytes once serialised`;
4533
+ }
4534
+ return { name, properties: JSON.parse(serialised) };
4535
+ }
4536
+ function byteLength(text) {
4537
+ return typeof TextEncoder === "function" ? new TextEncoder().encode(text).length : text.length * 3;
4538
+ }
4539
+
3920
4540
  // src/sampling.ts
3921
4541
  function hashToUnitInterval(input) {
3922
4542
  let hash = 2166136261;
@@ -3934,6 +4554,17 @@ function isSampledIn(visitorId, sampleRate) {
3934
4554
 
3935
4555
  // src/transport.ts
3936
4556
  var MAX_STALE_RECOVERIES = 3;
4557
+ var EVENT_FULL_SNAPSHOT2 = 2;
4558
+ var EVENT_INCREMENTAL2 = 3;
4559
+ var MAX_BODY_BYTES = 512 * 1024;
4560
+ var TARGET_CHUNK_BYTES = 256 * 1024;
4561
+ var MIN_BODY_BYTES = 8 * 1024;
4562
+ var ENVELOPE_SLACK_BYTES = 1024;
4563
+ var MAX_CHUNKS_PER_FLUSH = 20;
4564
+ var BACKOFF_BASE_MS = 2e3;
4565
+ var BACKOFF_MAX_MS = 6e4;
4566
+ var MAX_RETRY_AFTER_MS = 10 * 6e4;
4567
+ var MAX_CONSECUTIVE_REFUSALS = 3;
3937
4568
  async function staleSeqHint(response, sent) {
3938
4569
  try {
3939
4570
  if (typeof response.json !== "function") return void 0;
@@ -3945,10 +4576,40 @@ async function staleSeqHint(response, sent) {
3945
4576
  return void 0;
3946
4577
  }
3947
4578
  }
4579
+ function retryAfterMs(response, now = Date.now()) {
4580
+ var _a, _b;
4581
+ let raw;
4582
+ try {
4583
+ raw = (_b = (_a = response.headers) == null ? void 0 : _a.get) == null ? void 0 : _b.call(_a, "Retry-After");
4584
+ } catch (e) {
4585
+ return void 0;
4586
+ }
4587
+ if (!raw) return void 0;
4588
+ const text = raw.trim();
4589
+ if (/^\d+(\.\d+)?$/.test(text)) return Math.min(MAX_RETRY_AFTER_MS, Number(text) * 1e3);
4590
+ const date = Date.parse(text);
4591
+ if (Number.isNaN(date)) return void 0;
4592
+ return Math.min(MAX_RETRY_AFTER_MS, Math.max(0, date - now));
4593
+ }
4594
+ function utf8Length(text) {
4595
+ let bytes = 0;
4596
+ for (let i = 0; i < text.length; i += 1) {
4597
+ const code = text.charCodeAt(i);
4598
+ if (code < 128) bytes += 1;
4599
+ else if (code < 2048) bytes += 2;
4600
+ else if (code >= 55296 && code <= 56319 && i + 1 < text.length) {
4601
+ const next = text.charCodeAt(i + 1);
4602
+ if (next >= 56320 && next <= 57343) {
4603
+ bytes += 4;
4604
+ i += 1;
4605
+ } else bytes += 3;
4606
+ } else bytes += 3;
4607
+ }
4608
+ return bytes;
4609
+ }
3948
4610
  var KEEPALIVE_BODY_LIMIT_BYTES = 56 * 1024;
3949
4611
  function withinKeepaliveLimit(payload) {
3950
- const bytes = typeof TextEncoder === "function" ? new TextEncoder().encode(payload).length : payload.length * 3;
3951
- return bytes <= KEEPALIVE_BODY_LIMIT_BYTES;
4612
+ return utf8Length(payload) <= KEEPALIVE_BODY_LIMIT_BYTES;
3952
4613
  }
3953
4614
  var Transport = class {
3954
4615
  constructor(options, context, hooks = {}) {
@@ -3956,27 +4617,52 @@ var Transport = class {
3956
4617
  this.context = context;
3957
4618
  this.hooks = hooks;
3958
4619
  __publicField(this, "buffer", []);
4620
+ /** The chunk being delivered: built, numbered, maybe already attempted. */
4621
+ __publicField(this, "pending");
4622
+ /** The next unused chunk number. */
3959
4623
  __publicField(this, "seq", 0);
3960
4624
  __publicField(this, "consecutiveFailures", 0);
4625
+ __publicField(this, "consecutiveRefusals", 0);
3961
4626
  __publicField(this, "state", "open");
3962
4627
  __publicField(this, "timer");
4628
+ __publicField(this, "retryTimer");
3963
4629
  __publicField(this, "pendingMeta");
3964
4630
  __publicField(this, "flags", {});
3965
4631
  __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
4632
  __publicField(this, "staleRecoveries", 0);
4633
+ /** Our own backoff after a failure. Cleared when the network comes back. */
4634
+ __publicField(this, "backoffUntil", 0);
4635
+ /** What the server asked for in `Retry-After`. Not cleared by anything but time. */
4636
+ __publicField(this, "serverWaitUntil", 0);
4637
+ /** Largest body this transport will build; lowered by a `413`. */
4638
+ __publicField(this, "bodyLimit", MAX_BODY_BYTES);
4639
+ __publicField(this, "targetBytes", TARGET_CHUNK_BYTES);
4640
+ /**
4641
+ * Set when events were dropped and a full snapshot has been asked for:
4642
+ * DOM changes are not kept until it arrives.
4643
+ */
4644
+ __publicField(this, "awaitingSnapshot", false);
4645
+ /** Serialised events, so a retry or a re-split does not stringify a snapshot twice. */
4646
+ __publicField(this, "serialised", /* @__PURE__ */ new WeakMap());
3969
4647
  var _a;
3970
4648
  this.seq = (_a = context.initialSeq) != null ? _a : 0;
3971
4649
  }
3972
4650
  get isStopped() {
3973
4651
  return this.state === "stopped";
3974
4652
  }
4653
+ /** Events not yet delivered, including a chunk waiting to be retried. */
3975
4654
  get bufferedCount() {
3976
- return this.buffer.length;
4655
+ var _a, _b;
4656
+ return this.buffer.length + ((_b = (_a = this.pending) == null ? void 0 : _a.events.length) != null ? _b : 0);
3977
4657
  }
4658
+ /** The number the next undelivered chunk will carry — the count of chunks delivered, for a fresh session. */
3978
4659
  get sentChunks() {
3979
- return this.seq;
4660
+ var _a, _b;
4661
+ return (_b = (_a = this.pending) == null ? void 0 : _a.seq) != null ? _b : this.seq;
4662
+ }
4663
+ /** The most events kept while deliveries fail. Older ones are dropped first. */
4664
+ get bufferLimit() {
4665
+ return this.options.maxEventsPerChunk * 10;
3980
4666
  }
3981
4667
  /** Attaches metadata to the next chunk. Sent once, with the session's first chunk. */
3982
4668
  setMeta(meta) {
@@ -3995,22 +4681,131 @@ var Transport = class {
3995
4681
  var _a, _b;
3996
4682
  this.state = "stopped";
3997
4683
  if (this.timer) clearInterval(this.timer);
4684
+ if (this.retryTimer) clearTimeout(this.retryTimer);
3998
4685
  this.timer = void 0;
4686
+ this.retryTimer = void 0;
3999
4687
  this.buffer = [];
4688
+ this.pending = void 0;
4000
4689
  (_b = (_a = this.hooks).onStopped) == null ? void 0 : _b.call(_a, reason);
4001
4690
  }
4002
4691
  push(event) {
4003
4692
  if (this.state === "stopped") return;
4693
+ if (this.awaitingSnapshot) {
4694
+ if (event.type === EVENT_INCREMENTAL2) return;
4695
+ if (event.type === EVENT_FULL_SNAPSHOT2) this.awaitingSnapshot = false;
4696
+ }
4004
4697
  this.buffer.push(event);
4698
+ if (this.bufferedCount > this.bufferLimit) this.dropOldest(this.bufferedCount - this.bufferLimit);
4005
4699
  if (this.buffer.length >= this.options.maxEventsPerChunk) void this.flush();
4006
4700
  }
4007
- body(events) {
4701
+ /**
4702
+ * The network is back (`online`), or the page is visible again: try now
4703
+ * rather than at the end of the backoff. A server's `Retry-After` still
4704
+ * holds — it was not about our connection.
4705
+ */
4706
+ resume() {
4707
+ if (this.state === "stopped") return;
4708
+ this.backoffUntil = 0;
4709
+ void this.flush();
4710
+ }
4711
+ /**
4712
+ * Sends whatever is buffered, as as many chunks as it takes. Safe to call
4713
+ * when there is nothing to send, and while waiting out a failure, when it
4714
+ * does nothing. `force` skips our own backoff (an explicit `flush()` from
4715
+ * the page) but never the server's `Retry-After`.
4716
+ */
4717
+ async flush(force = false) {
4718
+ var _a;
4719
+ if (this.state === "stopped" || this.inFlight) return;
4720
+ const now = Date.now();
4721
+ if (now < this.serverWaitUntil) return;
4722
+ if (!force && now < this.backoffUntil) return;
4723
+ if (!this.pending && this.buffer.length === 0) return;
4724
+ this.inFlight = true;
4725
+ try {
4726
+ for (let sent = 0, tries = 0; sent < MAX_CHUNKS_PER_FLUSH && tries < MAX_CHUNKS_PER_FLUSH * 3 && this.state === "open"; tries += 1) {
4727
+ const chunk = (_a = this.pending) != null ? _a : this.nextChunk(this.bodyLimit, this.targetBytes);
4728
+ if (!chunk) break;
4729
+ this.pending = chunk;
4730
+ const outcome = await this.send(chunk);
4731
+ if (outcome === "sent") sent += 1;
4732
+ else if (outcome !== "again") break;
4733
+ }
4734
+ } finally {
4735
+ this.inFlight = false;
4736
+ }
4737
+ }
4738
+ /**
4739
+ * Builds the next chunk from the front of the buffer: at most
4740
+ * `maxEventsPerChunk` events and `target` bytes, or one event alone up to
4741
+ * `limit`. An event too large for any chunk is dropped here — it would be
4742
+ * refused, and refused again, and hold up everything behind it. For a
4743
+ * beacon (`forBeacon`) it is left where it is instead: an ordinary request
4744
+ * can still carry what a beacon cannot.
4745
+ */
4746
+ nextChunk(limit, target, forBeacon = false) {
4747
+ const envelope = utf8Length(this.envelope(this.seq)) + ENVELOPE_SLACK_BYTES;
4748
+ const events = [];
4749
+ const parts = [];
4750
+ let bytes = envelope;
4751
+ while (this.buffer.length > 0 && events.length < this.options.maxEventsPerChunk) {
4752
+ const event = this.buffer[0];
4753
+ const part = this.serialise(event);
4754
+ const size = utf8Length(part) + 1;
4755
+ if (events.length === 0 && bytes + size > limit) {
4756
+ if (forBeacon) return void 0;
4757
+ this.buffer.shift();
4758
+ if (!this.dropOversized(event, size)) return void 0;
4759
+ continue;
4760
+ }
4761
+ if (events.length > 0 && bytes + size > target) break;
4762
+ events.push(event);
4763
+ parts.push(part);
4764
+ bytes += size;
4765
+ this.buffer.shift();
4766
+ }
4767
+ if (events.length === 0) return void 0;
4768
+ const chunk = { seq: this.seq, events, parts, attempts: 0 };
4769
+ this.seq += 1;
4770
+ return chunk;
4771
+ }
4772
+ /**
4773
+ * An event that no chunk can carry. A DOM change or a custom event is
4774
+ * dropped, and the DOM is re-sent whole if the dropped one described it.
4775
+ * A full snapshot that alone exceeds the limit is a page this transport
4776
+ * cannot record — re-taking it would only be refused again — so recording
4777
+ * stops instead of uploading a snapshot every few seconds forever.
4778
+ */
4779
+ dropOversized(event, bytes) {
4780
+ var _a, _b, _c, _d;
4781
+ const kb = Math.ceil(bytes / 1024);
4782
+ if (event.type === EVENT_FULL_SNAPSHOT2) {
4783
+ (_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`);
4784
+ this.stop("snapshot_too_large");
4785
+ return false;
4786
+ }
4787
+ (_d = (_c = this.hooks).onWarning) == null ? void 0 : _d.call(_c, `dropped one event of ${kb} KB: larger than ingest accepts`);
4788
+ if (event.type === EVENT_INCREMENTAL2) this.afterGap();
4789
+ return true;
4790
+ }
4791
+ serialise(event) {
4792
+ let part = this.serialised.get(event);
4793
+ if (part === void 0) {
4794
+ part = JSON.stringify(event);
4795
+ this.serialised.set(event, part);
4796
+ }
4797
+ return part;
4798
+ }
4799
+ /** Everything in a chunk's body except its events, as a JSON object. */
4800
+ envelope(seq) {
4008
4801
  return JSON.stringify({
4009
4802
  projectKey: this.context.projectKey,
4010
4803
  sessionId: this.context.sessionId,
4011
4804
  visitorId: this.context.visitorId,
4012
- seq: this.seq,
4013
- events,
4805
+ seq,
4806
+ // The app this page runs inside, on every request: ingest reads it only
4807
+ // when the request has no real web origin. See `appId` in config.ts.
4808
+ ...this.options.appId ? { appId: this.options.appId } : {},
4014
4809
  ...this.pendingMeta ? { meta: this.pendingMeta } : {},
4015
4810
  ...Object.keys(this.flags).length > 0 ? { flags: this.flags } : {},
4016
4811
  // This device's clock as the request leaves, on the same clock as the
@@ -4022,20 +4817,24 @@ var Transport = class {
4022
4817
  sentAt: Date.now()
4023
4818
  });
4024
4819
  }
4025
- /** Sends whatever is buffered. Safe to call when there is nothing to send. */
4026
- async flush() {
4027
- var _a, _b, _c, _d;
4028
- if (this.state === "stopped" || this.inFlight || this.buffer.length === 0) return;
4029
- this.inFlight = true;
4030
- const events = this.buffer;
4031
- this.buffer = [];
4032
- let resend = false;
4820
+ /**
4821
+ * The body, assembled from the events' cached JSON rather than by
4822
+ * stringifying them again: a retry resends exactly the bytes the first
4823
+ * attempt sent, which is also what lets ingest read it as a retry.
4824
+ */
4825
+ body(chunk) {
4826
+ return `{"events":[${chunk.parts.join(",")}],${this.envelope(chunk.seq).slice(1)}`;
4827
+ }
4828
+ /** One request, and what to do next. */
4829
+ async send(chunk) {
4830
+ var _a, _b, _c, _d, _e, _f;
4831
+ const retry = chunk.attempts > 0;
4832
+ chunk.attempts += 1;
4833
+ (_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, chunk.seq + 1);
4834
+ const payload = this.body(chunk);
4835
+ let response;
4033
4836
  try {
4034
- const retry = this.lastAttemptedSeq === this.seq;
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`, {
4837
+ response = await fetch(`${this.options.ingestUrl}/v1/ingest/events`, {
4039
4838
  method: "POST",
4040
4839
  headers: { "Content-Type": "application/json" },
4041
4840
  body: payload,
@@ -4044,84 +4843,215 @@ var Transport = class {
4044
4843
  credentials: "omit",
4045
4844
  keepalive: withinKeepaliveLimit(payload)
4046
4845
  });
4047
- if (response.ok) {
4048
- const nextSeq = retry ? void 0 : await staleSeqHint(response, this.seq);
4049
- if (nextSeq !== void 0 && this.staleRecoveries < MAX_STALE_RECOVERIES) {
4050
- this.staleRecoveries += 1;
4051
- this.seq = nextSeq;
4052
- this.requeue(events);
4053
- resend = true;
4054
- return;
4055
- }
4056
- this.staleRecoveries = 0;
4057
- this.seq += 1;
4058
- this.pendingMeta = void 0;
4059
- this.consecutiveFailures = 0;
4060
- (_d = (_c = this.hooks).onSent) == null ? void 0 : _d.call(_c, this.seq - 1, events.length);
4061
- return;
4846
+ } catch (e) {
4847
+ return this.registerFailure(void 0);
4848
+ }
4849
+ if (this.state === "stopped" || this.pending !== chunk) return "stopped";
4850
+ if (response.ok) {
4851
+ const nextSeq = retry ? void 0 : await staleSeqHint(response, chunk.seq);
4852
+ if (nextSeq !== void 0 && this.staleRecoveries < MAX_STALE_RECOVERIES) {
4853
+ this.staleRecoveries += 1;
4854
+ chunk.seq = nextSeq;
4855
+ chunk.attempts = 0;
4856
+ this.seq = Math.max(this.seq, nextSeq + 1);
4857
+ return "again";
4062
4858
  }
4063
- if (response.status >= 400 && response.status < 500 && response.status !== 429) {
4064
- this.stop(`rejected_${response.status}`);
4065
- return;
4859
+ this.pending = void 0;
4860
+ this.staleRecoveries = 0;
4861
+ this.pendingMeta = void 0;
4862
+ this.consecutiveFailures = 0;
4863
+ this.consecutiveRefusals = 0;
4864
+ this.backoffUntil = 0;
4865
+ (_d = (_c = this.hooks).onSent) == null ? void 0 : _d.call(_c, chunk.seq, chunk.events.length);
4866
+ return "sent";
4867
+ }
4868
+ const status = response.status;
4869
+ if (status === 413) return this.shrink(chunk, utf8Length(payload));
4870
+ if (status === 400 || status === 422) {
4871
+ this.pending = void 0;
4872
+ this.consecutiveRefusals += 1;
4873
+ if (this.consecutiveRefusals >= MAX_CONSECUTIVE_REFUSALS) {
4874
+ this.stop(`rejected_${status}`);
4875
+ return "stopped";
4066
4876
  }
4067
- this.requeue(events);
4068
- this.registerFailure();
4069
- } catch (e) {
4070
- this.requeue(events);
4071
- this.registerFailure();
4072
- } finally {
4073
- this.inFlight = false;
4074
- if (resend) void this.flush();
4877
+ (_f = (_e = this.hooks).onWarning) == null ? void 0 : _f.call(_e, `ingest refused a chunk (${status}); its ${chunk.events.length} events are dropped`);
4878
+ this.afterGap();
4879
+ return "again";
4880
+ }
4881
+ if (status === 408 || status === 429 || status >= 500) {
4882
+ return this.registerFailure(retryAfterMs(response));
4075
4883
  }
4884
+ this.stop(`rejected_${status}`);
4885
+ return "stopped";
4076
4886
  }
4077
4887
  /**
4078
- * Puts a failed batch back at the front of the queue.
4079
- *
4080
- * Bounded on purpose: a long offline period must not grow the buffer until
4081
- * the tab runs out of memory. Older events are dropped first — a replay
4082
- * missing its beginning is still useful; a crashed page is not.
4888
+ * `413`: this ingest takes less than we thought. The chunk was not stored,
4889
+ * so its events go back to the front of the queue and are re-cut under a
4890
+ * lower limit; its number is not reused, so no number ever carries two
4891
+ * different sets of events. A chunk of one event that is still too large is
4892
+ * dropped by `nextChunk` on the way back out.
4083
4893
  */
4084
- requeue(events) {
4085
- const limit = this.options.maxEventsPerChunk * 10;
4086
- this.buffer = [...events, ...this.buffer].slice(-limit);
4894
+ shrink(chunk, bytes) {
4895
+ var _a, _b;
4896
+ this.pending = void 0;
4897
+ if (this.bodyLimit <= MIN_BODY_BYTES) {
4898
+ this.stop("rejected_413");
4899
+ return "stopped";
4900
+ }
4901
+ this.buffer = [...chunk.events, ...this.buffer];
4902
+ this.bodyLimit = Math.max(MIN_BODY_BYTES, Math.min(this.bodyLimit, Math.floor(bytes / 2)));
4903
+ this.targetBytes = Math.min(this.targetBytes, this.bodyLimit);
4904
+ (_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`);
4905
+ return "again";
4087
4906
  }
4088
- registerFailure() {
4907
+ /**
4908
+ * Counts a failed delivery and schedules the next attempt: 2 s, 4 s, 8 s …
4909
+ * up to a minute, each with jitter so a thousand tabs that lost the same
4910
+ * Wi-Fi do not all come back in the same second. The chunk stays pending,
4911
+ * to be resent under the same number.
4912
+ *
4913
+ * Recording does not stop over this unless the page asked for it with
4914
+ * `maxConsecutiveFailures`: the buffer exists for exactly this, and a
4915
+ * phone in a tunnel for half a minute must not end the visit's recording.
4916
+ */
4917
+ registerFailure(serverWaitMs) {
4089
4918
  this.consecutiveFailures += 1;
4090
- if (this.consecutiveFailures >= this.options.maxConsecutiveFailures) {
4919
+ const limit = this.options.maxConsecutiveFailures;
4920
+ if (limit > 0 && this.consecutiveFailures >= limit) {
4091
4921
  this.stop("too_many_failures");
4922
+ return "stopped";
4923
+ }
4924
+ const now = Date.now();
4925
+ const ceiling = Math.min(BACKOFF_MAX_MS, BACKOFF_BASE_MS * 2 ** Math.min(this.consecutiveFailures - 1, 16));
4926
+ const delay = ceiling / 2 + Math.random() * (ceiling / 2);
4927
+ this.backoffUntil = now + delay;
4928
+ if (serverWaitMs !== void 0) this.serverWaitUntil = now + serverWaitMs;
4929
+ this.scheduleRetry(Math.max(this.backoffUntil, this.serverWaitUntil) - now);
4930
+ return "wait";
4931
+ }
4932
+ /**
4933
+ * The attempt at the end of a backoff. It skips the backoff check itself:
4934
+ * a timer may fire a fraction of a millisecond "early" against `Date.now`,
4935
+ * and refusing then would leave nothing scheduled until the next tick.
4936
+ */
4937
+ scheduleRetry(delayMs) {
4938
+ var _a, _b;
4939
+ if (this.retryTimer) clearTimeout(this.retryTimer);
4940
+ this.retryTimer = setTimeout(() => {
4941
+ this.retryTimer = void 0;
4942
+ void this.flush(true);
4943
+ }, Math.max(0, Math.ceil(delayMs)));
4944
+ (_b = (_a = this.retryTimer).unref) == null ? void 0 : _b.call(_a);
4945
+ }
4946
+ /**
4947
+ * Keeps the buffer bounded during a long outage. Older events go first — a
4948
+ * replay missing a stretch is still useful; a crashed page is not. A chunk
4949
+ * waiting to be retried is the oldest of all, and goes whole: sending part
4950
+ * of it under its number could clash with an attempt that did arrive.
4951
+ */
4952
+ dropOldest(count) {
4953
+ var _a, _b;
4954
+ let remaining = count;
4955
+ if (this.pending && !this.inFlight) {
4956
+ remaining -= this.pending.events.length;
4957
+ this.pending = void 0;
4958
+ }
4959
+ if (remaining > 0) this.buffer.splice(0, remaining);
4960
+ (_b = (_a = this.hooks).onWarning) == null ? void 0 : _b.call(_a, `dropped ${count} buffered events while ingest could not be reached`);
4961
+ this.afterGap();
4962
+ }
4963
+ /**
4964
+ * After events were dropped: DOM changes left before the next full snapshot
4965
+ * may describe nodes whose creation was dropped, and a replay applying them
4966
+ * breaks. They go, and when the buffer holds no snapshot to start again
4967
+ * from, the recorder is asked for one. Custom events, errors and the like
4968
+ * do not depend on the DOM and stay.
4969
+ */
4970
+ afterGap() {
4971
+ const snapshot2 = this.buffer.findIndex((e) => e.type === EVENT_FULL_SNAPSHOT2);
4972
+ const end = snapshot2 === -1 ? this.buffer.length : snapshot2;
4973
+ this.buffer = [
4974
+ ...this.buffer.slice(0, end).filter((e) => e.type !== EVENT_INCREMENTAL2),
4975
+ ...this.buffer.slice(end)
4976
+ ];
4977
+ if (snapshot2 === -1 && !this.awaitingSnapshot && this.hooks.onResyncNeeded) {
4978
+ this.awaitingSnapshot = true;
4979
+ this.hooks.onResyncNeeded();
4092
4980
  }
4093
4981
  }
4094
4982
  /**
4095
- * Last-gasp send when the page is going away.
4983
+ * Last-gasp send when the page is going away or hidden.
4096
4984
  *
4097
4985
  * `fetch` is cancelled once the document unloads, so the tail of every
4098
4986
  * session would be lost. `sendBeacon` hands the payload to the browser, which
4099
- * delivers it after the page is gone.
4987
+ * delivers it after the page is gone — but only up to 64 KB in flight, so
4988
+ * the tail goes as a run of small chunks, oldest first, until the browser
4989
+ * says it has no room. What it refuses stays buffered for the page's next
4990
+ * flush, should there be one (a hidden tab that comes back, a page restored
4991
+ * from the back/forward cache).
4992
+ *
4993
+ * For a tab that is only hidden, and so likely to come back, nothing goes
4994
+ * while deliveries are failing or the server asked us to wait: a beacon
4995
+ * "queued" while offline is usually a beacon lost, where the buffer and the
4996
+ * backoff would have delivered it. On `pagehide` (`final`), the last
4997
+ * chance there is, it goes regardless.
4100
4998
  */
4101
- flushWithBeacon() {
4102
- var _a, _b;
4103
- if (this.state === "stopped" || this.buffer.length === 0) return false;
4104
- const events = this.buffer;
4105
- const payload = this.body(events);
4106
- this.buffer = [];
4999
+ flushWithBeacon(final = false) {
5000
+ if (this.state === "stopped" || !this.pending && this.buffer.length === 0) return false;
5001
+ if (!final && (this.consecutiveFailures > 0 || Date.now() < this.serverWaitUntil)) return false;
5002
+ if (typeof navigator === "undefined" || typeof navigator.sendBeacon !== "function") return false;
5003
+ const url = `${this.options.ingestUrl}/v1/ingest/events`;
5004
+ let queuedAny = false;
5005
+ let tooLarge = false;
4107
5006
  try {
4108
- if (typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function") {
4109
- (_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, this.seq + 1);
4110
- const blob = new Blob([payload], { type: "text/plain;charset=UTF-8" });
4111
- const queued = navigator.sendBeacon(`${this.options.ingestUrl}/v1/ingest/events`, blob);
4112
- if (queued) {
4113
- this.seq += 1;
4114
- this.pendingMeta = void 0;
4115
- return true;
5007
+ const pending = this.pending;
5008
+ if (pending) {
5009
+ const payload = this.body(pending);
5010
+ if (!withinKeepaliveLimit(payload)) {
5011
+ tooLarge = true;
5012
+ } else {
5013
+ if (!this.beacon(url, pending, payload)) return queuedAny;
5014
+ queuedAny = true;
5015
+ if (!this.inFlight) {
5016
+ this.pending = void 0;
5017
+ this.pendingMeta = void 0;
5018
+ }
5019
+ }
5020
+ }
5021
+ while (!tooLarge && this.buffer.length > 0) {
5022
+ const chunk = this.nextChunk(KEEPALIVE_BODY_LIMIT_BYTES, KEEPALIVE_BODY_LIMIT_BYTES, true);
5023
+ if (!chunk) {
5024
+ tooLarge = true;
5025
+ break;
5026
+ }
5027
+ const payload = this.body(chunk);
5028
+ if (!this.beacon(url, chunk, payload)) {
5029
+ this.buffer = [...chunk.events, ...this.buffer];
5030
+ this.seq = chunk.seq;
5031
+ break;
4116
5032
  }
5033
+ queuedAny = true;
5034
+ this.pendingMeta = void 0;
4117
5035
  }
4118
5036
  } catch (e) {
4119
5037
  }
4120
- this.buffer = events;
4121
- return false;
5038
+ if (tooLarge) void this.flush(true);
5039
+ return queuedAny;
5040
+ }
5041
+ /** Hands one chunk to the browser. False when its beacon queue is full. */
5042
+ beacon(url, chunk, payload) {
5043
+ var _a, _b;
5044
+ (_b = (_a = this.hooks).onSeqReserved) == null ? void 0 : _b.call(_a, chunk.seq + 1);
5045
+ const blob = new Blob([payload], { type: "text/plain;charset=UTF-8" });
5046
+ return navigator.sendBeacon(url, blob);
4122
5047
  }
4123
5048
  };
4124
5049
 
5050
+ // src/version.ts
5051
+ var SDK_NAME = "@anyreplay/browser";
5052
+ var SDK_VERSION = true ? "0.2.0" : "0.0.0-dev";
5053
+ var PLATFORM = "web";
5054
+
4125
5055
  // src/visitor.ts
4126
5056
  var VISITOR_KEY = "anyreplay.vid";
4127
5057
  var SESSION_KEY = "anyreplay.sid";
@@ -4231,7 +5161,7 @@ var RAGE_CLICK_WINDOW_MS = 1e3;
4231
5161
  var RAGE_CLICK_RADIUS_PX = 30;
4232
5162
  var MAX_PENDING_EVENTS = 32;
4233
5163
  var EDITABLE_SELECTOR = '[contenteditable]:not([contenteditable="false"])';
4234
- function createRecorder(rawOptions) {
5164
+ function createRecorder(rawOptions, shell) {
4235
5165
  const options = resolveOptions(rawOptions);
4236
5166
  const local = safeStorage(() => globalThis.localStorage);
4237
5167
  const session = safeStorage(() => globalThis.sessionStorage);
@@ -4239,6 +5169,8 @@ function createRecorder(rawOptions) {
4239
5169
  let identity;
4240
5170
  let transport;
4241
5171
  let stopRecording;
5172
+ let diagnostics;
5173
+ let assets;
4242
5174
  let pageCount = 0;
4243
5175
  let pendingTraits;
4244
5176
  const sendIdentity = (who, traits) => {
@@ -4246,37 +5178,49 @@ function createRecorder(rawOptions) {
4246
5178
  method: "POST",
4247
5179
  headers: { "Content-Type": "application/json" },
4248
5180
  credentials: "omit",
4249
- body: JSON.stringify({ projectKey: options.projectKey, sessionId: who.sessionId, ...traits })
5181
+ body: JSON.stringify({
5182
+ projectKey: options.projectKey,
5183
+ sessionId: who.sessionId,
5184
+ ...traits,
5185
+ ...options.appId ? { appId: options.appId } : {}
5186
+ })
4250
5187
  }).catch(() => {
4251
5188
  });
4252
5189
  };
4253
- const collectMeta = (who) => ({
4254
- startedAt: Date.now(),
4255
- url: safeLocation(),
4256
- lang: typeof navigator !== "undefined" ? navigator.language : void 0,
4257
- userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
4258
- screenWidth: typeof window !== "undefined" ? window.innerWidth : void 0,
4259
- screenHeight: typeof window !== "undefined" ? window.innerHeight : void 0,
4260
- referrer: typeof document !== "undefined" ? document.referrer || void 0 : void 0,
4261
- // The cap is a decision about starting a session, so it goes only where
4262
- // ingest makes that decision: chunk 0. A resumed page starts later.
4263
- ...options.maxSessionsPerMonth !== void 0 && who.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
4264
- });
5190
+ const collectMeta = (who) => {
5191
+ var _a, _b;
5192
+ return {
5193
+ startedAt: Date.now(),
5194
+ url: safeLocation(),
5195
+ lang: typeof navigator !== "undefined" ? navigator.language : void 0,
5196
+ userAgent: typeof navigator !== "undefined" ? navigator.userAgent : void 0,
5197
+ screenWidth: typeof window !== "undefined" ? window.innerWidth : void 0,
5198
+ screenHeight: typeof window !== "undefined" ? window.innerHeight : void 0,
5199
+ referrer: typeof document !== "undefined" ? document.referrer || void 0 : void 0,
5200
+ platform: (_a = shell == null ? void 0 : shell.platform) != null ? _a : PLATFORM,
5201
+ sdk: (_b = shell == null ? void 0 : shell.sdk) != null ? _b : { name: SDK_NAME, version: SDK_VERSION },
5202
+ ...(shell == null ? void 0 : shell.appVersion) ? { appVersion: shell.appVersion.slice(0, 40) } : {},
5203
+ ...(shell == null ? void 0 : shell.deviceModel) ? { deviceModel: shell.deviceModel.slice(0, 64) } : {},
5204
+ // The cap is a decision about starting a session, so it goes only where
5205
+ // ingest makes that decision: chunk 0. A resumed page starts later.
5206
+ ...options.maxSessionsPerMonth !== void 0 && who.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
5207
+ };
5208
+ };
4265
5209
  const recentClicks = [];
4266
5210
  const noteInteraction = (event, channel) => {
4267
5211
  var _a;
4268
5212
  const data = event.data;
4269
- if (event.type !== 3 || (data == null ? void 0 : data.source) !== 2 || typeof data.x !== "number") return;
5213
+ if (event.type !== 3 || (data == null ? void 0 : data.source) !== 2 || data.type !== 2 || typeof data.x !== "number") return;
4270
5214
  const now = event.timestamp;
4271
5215
  recentClicks.push({ x: data.x, y: (_a = data.y) != null ? _a : 0, t: now });
4272
5216
  while (recentClicks.length > 0 && now - recentClicks[0].t > RAGE_CLICK_WINDOW_MS) recentClicks.shift();
4273
- if (recentClicks.length >= RAGE_CLICK_COUNT) {
4274
- const first = recentClicks[0];
4275
- const clustered = recentClicks.every(
4276
- (c) => Math.abs(c.x - first.x) < RAGE_CLICK_RADIUS_PX && Math.abs(c.y - first.y) < RAGE_CLICK_RADIUS_PX
4277
- );
4278
- if (clustered) channel.mergeFlags({ hasRageClick: true });
4279
- }
5217
+ const near = recentClicks.filter(
5218
+ (c) => {
5219
+ var _a2;
5220
+ return Math.abs(c.x - data.x) < RAGE_CLICK_RADIUS_PX && Math.abs(c.y - ((_a2 = data.y) != null ? _a2 : 0)) < RAGE_CLICK_RADIUS_PX;
5221
+ }
5222
+ );
5223
+ if (near.length >= RAGE_CLICK_COUNT) channel.mergeFlags({ hasRageClick: true });
4280
5224
  };
4281
5225
  const pending = [];
4282
5226
  let draining = false;
@@ -4289,6 +5233,7 @@ function createRecorder(rawOptions) {
4289
5233
  const next = pending[0];
4290
5234
  record.addCustomEvent(next.tag, next.payload);
4291
5235
  pending.shift();
5236
+ if (next.tag === ERROR_TAG) transport == null ? void 0 : transport.mergeFlags({ hasError: true });
4292
5237
  }
4293
5238
  } catch (e) {
4294
5239
  if (!drainScheduled && status === "recording") {
@@ -4355,17 +5300,52 @@ function createRecorder(rawOptions) {
4355
5300
  status = "stopped";
4356
5301
  stopRecording == null ? void 0 : stopRecording();
4357
5302
  stopRecording = void 0;
5303
+ diagnostics == null ? void 0 : diagnostics.stop();
5304
+ diagnostics = void 0;
5305
+ assets == null ? void 0 : assets.stop();
5306
+ assets = void 0;
4358
5307
  if (reason === "rejected_402") startCooldown(session);
4359
5308
  if (options.debug) console.warn(`[anyreplay] recording stopped: ${reason}`);
4360
- }
5309
+ },
5310
+ onWarning: (message) => {
5311
+ if (options.debug) console.warn(`[anyreplay] ${message}`);
5312
+ },
5313
+ // Events were dropped (a long outage, or one too large to send), so
5314
+ // the DOM changes after the gap describe nodes ingest never got. A
5315
+ // fresh full snapshot starts the page over. Taken on the next turn,
5316
+ // not here: this can be called from inside rrweb's own emit, in the
5317
+ // middle of handing over a batch of mutations.
5318
+ onResyncNeeded: () => setTimeout(() => takeSnapshot(2), 0)
4361
5319
  }
4362
5320
  );
4363
5321
  transport = channel;
4364
5322
  status = "recording";
5323
+ const takeSnapshot = (triesLeft) => {
5324
+ if (status !== "recording" || !stopRecording) return;
5325
+ try {
5326
+ record.takeFullSnapshot(true);
5327
+ } catch (e) {
5328
+ if (triesLeft > 0) setTimeout(() => takeSnapshot(triesLeft - 1), 1e3);
5329
+ }
5330
+ };
4365
5331
  lastUrl = safeLocation();
4366
5332
  channel.setMeta(collectMeta(who));
4367
5333
  channel.mergeFlags({ pageCount });
4368
5334
  channel.start();
5335
+ const prefix = options.assets === "inline" ? localAssetPrefix() : null;
5336
+ if (prefix && typeof fetch === "function") {
5337
+ assets = new AssetCollector({
5338
+ prefix,
5339
+ ingestUrl: options.ingestUrl,
5340
+ projectKey: options.projectKey,
5341
+ appId: options.appId,
5342
+ // Bound now, before the network diagnostics wrap `fetch`: reading the
5343
+ // app's own files is the recorder's business, not the app's traffic.
5344
+ fetch: fetch.bind(globalThis),
5345
+ emit: (payload) => emitCustom(ASSET_TAG, payload),
5346
+ debug: options.debug
5347
+ });
5348
+ }
4369
5349
  const policy = {
4370
5350
  maskEveryField: options.maskAllInputs || options.maskAllTyping,
4371
5351
  maskClass: options.maskClass
@@ -4374,6 +5354,7 @@ function createRecorder(rawOptions) {
4374
5354
  emit: (event) => {
4375
5355
  const typed = event;
4376
5356
  scrubEvent(typed, (id) => record.mirror.getNode(id), policy);
5357
+ assets == null ? void 0 : assets.scan(typed);
4377
5358
  noteInteraction(typed, channel);
4378
5359
  touchSession(session, typed.timestamp);
4379
5360
  channel.push(typed);
@@ -4383,6 +5364,9 @@ function createRecorder(rawOptions) {
4383
5364
  maskTextClass: options.maskClass,
4384
5365
  maskTextSelector: options.maskAllTyping ? EDITABLE_SELECTOR : void 0,
4385
5366
  blockClass: options.blockClass,
5367
+ // Readable stylesheets go into the snapshot as text, in both asset
5368
+ // modes. rrweb's default, written out because 'inline' depends on it.
5369
+ inlineStylesheet: true,
4386
5370
  // Canvas capture is expensive and off unless a customer opts in later.
4387
5371
  recordCanvas: false,
4388
5372
  collectFonts: false
@@ -4397,13 +5381,21 @@ function createRecorder(rawOptions) {
4397
5381
  const attachLifecycleHooks = (channel) => {
4398
5382
  if (typeof window === "undefined") return;
4399
5383
  window.addEventListener("pagehide", () => {
4400
- channel.flushWithBeacon();
5384
+ channel.flushWithBeacon(true);
4401
5385
  }, { capture: true });
4402
5386
  document.addEventListener("visibilitychange", () => {
4403
5387
  if (document.visibilityState === "hidden") channel.flushWithBeacon();
5388
+ else channel.resume();
5389
+ });
5390
+ window.addEventListener("online", () => channel.resume());
5391
+ diagnostics = startDiagnostics({
5392
+ errors: options.recordErrors,
5393
+ console: options.recordConsole,
5394
+ network: options.recordNetwork,
5395
+ ingestUrl: options.ingestUrl,
5396
+ emit: emitCustom,
5397
+ onError: () => channel.mergeFlags({ hasError: true })
4404
5398
  });
4405
- window.addEventListener("error", () => channel.mergeFlags({ hasError: true }));
4406
- window.addEventListener("unhandledrejection", () => channel.mergeFlags({ hasError: true }));
4407
5399
  const patch2 = (method) => {
4408
5400
  const original = history[method].bind(history);
4409
5401
  history[method] = function patched(...args) {
@@ -4433,8 +5425,12 @@ function createRecorder(rawOptions) {
4433
5425
  return;
4434
5426
  }
4435
5427
  transport == null ? void 0 : transport.stop("consent_withdrawn");
4436
- if (status === "idle" || status === "awaiting-consent") status = "stopped";
5428
+ if (status === "idle" || status === "awaiting-consent" || status === "sampled-out") {
5429
+ status = "awaiting-consent";
5430
+ identity = void 0;
5431
+ }
4437
5432
  pendingTraits = void 0;
5433
+ pending.length = 0;
4438
5434
  forgetVisitor(local, session);
4439
5435
  },
4440
5436
  identify: (traits) => {
@@ -4454,14 +5450,24 @@ function createRecorder(rawOptions) {
4454
5450
  }
4455
5451
  emitCustom(TRACK_TAG, payload);
4456
5452
  },
5453
+ trackError: (error) => {
5454
+ if (!options.recordErrors) return;
5455
+ if (diagnostics) {
5456
+ diagnostics.error(error);
5457
+ return;
5458
+ }
5459
+ emitCustom(ERROR_TAG, errorPayload("error", error));
5460
+ if (status === "recording") transport == null ? void 0 : transport.mergeFlags({ hasError: true });
5461
+ },
4457
5462
  stop: () => {
4458
5463
  transport == null ? void 0 : transport.stop("stopped_by_page");
4459
5464
  if (status === "idle" || status === "awaiting-consent") status = "stopped";
4460
5465
  pendingTraits = void 0;
4461
5466
  },
5467
+ // Asked for by the page, so it skips our own backoff (never a server's Retry-After).
4462
5468
  flush: () => {
4463
5469
  var _a;
4464
- return (_a = transport == null ? void 0 : transport.flush()) != null ? _a : Promise.resolve();
5470
+ return (_a = transport == null ? void 0 : transport.flush(true)) != null ? _a : Promise.resolve();
4465
5471
  }
4466
5472
  };
4467
5473
  }
@@ -4474,20 +5480,36 @@ function safeLocation() {
4474
5480
  }
4475
5481
  // Annotate the CommonJS export names for ESM import in node:
4476
5482
  0 && (module.exports = {
5483
+ ASSET_LIMITS,
5484
+ ASSET_TAG,
5485
+ CONSOLE_TAG,
4477
5486
  ConfigError,
4478
5487
  DEFAULTS,
5488
+ ERROR_TAG,
4479
5489
  EVENT_NAME_PATTERN,
5490
+ LIMITS,
4480
5491
  MAX_PROPERTIES_BYTES,
4481
5492
  NAVIGATE_TAG,
5493
+ NETWORK_TAG,
5494
+ REDACTED,
5495
+ SDK_NAME,
5496
+ SDK_VERSION,
4482
5497
  TRACK_TAG,
4483
5498
  Transport,
5499
+ consolePayload,
4484
5500
  createRecorder,
5501
+ defaultAssetMode,
5502
+ errorPayload,
4485
5503
  forgetVisitor,
4486
5504
  hashToUnitInterval,
4487
5505
  isSampledIn,
5506
+ localAssetPrefix,
5507
+ redactText,
5508
+ redactUrl,
4488
5509
  resolveIdentity,
4489
5510
  resolveOptions,
4490
5511
  safeStorage,
5512
+ startDiagnostics,
4491
5513
  uuidv4,
4492
5514
  validateTrack
4493
5515
  });