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