@anyreplay/browser 0.1.1 → 0.1.3

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
@@ -3641,10 +3641,12 @@ record.mirror = mirror;
3641
3641
  var DEFAULTS = {
3642
3642
  ingestUrl: "https://in.anyreplay.com",
3643
3643
  sampleRate: 1,
3644
- // Default-deny. Recording a customer's password field once is a breach; the
3645
- // cost of the safe default is a configuration line for the rare field that
3646
- // genuinely needs to be visible.
3647
- maskAllInputs: true,
3644
+ // Both masking switches are opt-in: a recording shows what people typed,
3645
+ // which is the point of watching one. What that never includes is the floor
3646
+ // in masking.ts — passwords, payment fields and the rest — which no option
3647
+ // can switch off.
3648
+ maskAllInputs: false,
3649
+ maskAllTyping: false,
3648
3650
  maskClass: "ar-mask",
3649
3651
  blockClass: "ar-block",
3650
3652
  requireConsent: false,
@@ -3677,11 +3679,18 @@ function resolveOptions(input) {
3677
3679
  projectKey: input.projectKey,
3678
3680
  sampleRate,
3679
3681
  ingestUrl: ((_c = input.ingestUrl) != null ? _c : DEFAULTS.ingestUrl).replace(/\/+$/, ""),
3680
- // A caller who explicitly passes false gets what they asked for; anything
3681
- // else (including undefined) keeps masking on.
3682
- maskAllInputs: input.maskAllInputs === false ? false : true
3682
+ maskAllInputs: readSwitch(input, "maskAllInputs"),
3683
+ maskAllTyping: readSwitch(input, "maskAllTyping")
3683
3684
  };
3684
3685
  }
3686
+ function readSwitch(input, name) {
3687
+ const value = input[name];
3688
+ if (typeof value === "boolean") return value;
3689
+ 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.`);
3691
+ }
3692
+ return DEFAULTS[name];
3693
+ }
3685
3694
 
3686
3695
  // src/events.ts
3687
3696
  var TRACK_TAG = "anyreplay.track";
@@ -3711,6 +3720,170 @@ function byteLength(text) {
3711
3720
  return typeof TextEncoder === "function" ? new TextEncoder().encode(text).length : text.length * 3;
3712
3721
  }
3713
3722
 
3723
+ // src/masking.ts
3724
+ var maskValue = (text) => "*".repeat(text.length);
3725
+ var MASK_EVERY_FIELD = {
3726
+ color: true,
3727
+ date: true,
3728
+ "datetime-local": true,
3729
+ email: true,
3730
+ month: true,
3731
+ number: true,
3732
+ range: true,
3733
+ search: true,
3734
+ tel: true,
3735
+ text: true,
3736
+ time: true,
3737
+ url: true,
3738
+ week: true,
3739
+ textarea: true,
3740
+ select: true,
3741
+ password: true,
3742
+ input: true
3743
+ };
3744
+ var SENSITIVE_AUTOCOMPLETE = /* @__PURE__ */ new Set([
3745
+ "cc-number",
3746
+ "cc-csc",
3747
+ "cc-exp",
3748
+ "cc-exp-month",
3749
+ "cc-exp-year",
3750
+ "cc-name",
3751
+ "cc-type",
3752
+ "cc-given-name",
3753
+ "cc-family-name",
3754
+ "cc-additional-name",
3755
+ "current-password",
3756
+ "new-password",
3757
+ "one-time-code"
3758
+ ]);
3759
+ var PAYMENT_FIELD = new RegExp(
3760
+ "(?:credit|debit|payment)[\\s_-]*card|card[\\s_-]*(?:number|num|no|nr|code|pin|cvv|cvc|csc)|(?:^|[^a-z])cc[\\s_-]*(?:number|num|no|nr|csc|cvv|cvc|exp)|(?:^|[^a-z])(?:cvv|cvc|cvn|csc)(?:[^a-z]|$)|security[\\s_-]*code|(?:^|[^a-z])iban(?:[^a-z]|$)|routing[\\s_-]*number|sort[\\s_-]*code",
3761
+ "i"
3762
+ );
3763
+ var CREDENTIAL_FIELD = /pass(?:word|wd|phrase)|(?:^|[^a-z])pwd(?:[^a-z]|$)|(?:^|[^a-z])otp(?:[^a-z]|$)|one[\s_-]*time[\s_-]*code/i;
3764
+ var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
3765
+ var MASK_SENSITIVE_FIELDS = Object.fromEntries(
3766
+ [...SENSITIVE_TYPES].map((type) => [type, true])
3767
+ );
3768
+ var NAMING_ATTRIBUTES = ["name", "id", "autocomplete", "placeholder", "aria-label", "data-testid"];
3769
+ function isSensitiveByAttributes(read, type) {
3770
+ var _a;
3771
+ if (read("data-rr-is-password") !== null) return true;
3772
+ if (SENSITIVE_TYPES.has(type)) return true;
3773
+ const autocomplete = ((_a = read("autocomplete")) != null ? _a : "").toLowerCase();
3774
+ for (const token of autocomplete.split(/\s+/)) {
3775
+ if (SENSITIVE_AUTOCOMPLETE.has(token)) return true;
3776
+ }
3777
+ for (const attribute of NAMING_ATTRIBUTES) {
3778
+ const value = read(attribute);
3779
+ if (value && (PAYMENT_FIELD.test(value) || CREDENTIAL_FIELD.test(value))) return true;
3780
+ }
3781
+ return false;
3782
+ }
3783
+ var asElement = (node) => node && typeof node.getAttribute === "function" ? node : null;
3784
+ function isSensitiveField(node) {
3785
+ const element = asElement(node);
3786
+ if (!element) return false;
3787
+ const type = (element.type || element.getAttribute("type") || "").toLowerCase();
3788
+ return isSensitiveByAttributes((name) => element.getAttribute(name), type);
3789
+ }
3790
+ function isSensitiveSerialised(attributes) {
3791
+ var _a;
3792
+ const read = (name) => typeof attributes[name] === "string" ? attributes[name] : null;
3793
+ return isSensitiveByAttributes(read, ((_a = read("type")) != null ? _a : "").toLowerCase());
3794
+ }
3795
+ function looksLikeCardNumber(text) {
3796
+ if (text.length < 13 || text.length > 24 || !/^[0-9][0-9 -]*[0-9]$/.test(text)) return false;
3797
+ const digits = text.replace(/[ -]/g, "");
3798
+ if (digits.length < 13 || digits.length > 19) return false;
3799
+ let sum = 0;
3800
+ let double = false;
3801
+ for (let i = digits.length - 1; i >= 0; i -= 1) {
3802
+ let digit = digits.charCodeAt(i) - 48;
3803
+ if (double) {
3804
+ digit *= 2;
3805
+ if (digit > 9) digit -= 9;
3806
+ }
3807
+ sum += digit;
3808
+ double = !double;
3809
+ }
3810
+ return sum % 10 === 0;
3811
+ }
3812
+ function isMarkedByCustomer(node, maskClass) {
3813
+ var _a;
3814
+ let element = asElement(node);
3815
+ while (element) {
3816
+ if ((_a = element.classList) == null ? void 0 : _a.contains(maskClass)) return true;
3817
+ element = element.parentElement;
3818
+ }
3819
+ return false;
3820
+ }
3821
+ var hasClass = (attributes, maskClass) => typeof attributes.class === "string" && attributes.class.split(/\s+/).includes(maskClass);
3822
+ function shouldMaskValue(text, node, policy) {
3823
+ if (policy.maskEveryField) return true;
3824
+ if (isSensitiveField(node)) return true;
3825
+ if (isMarkedByCustomer(node, policy.maskClass)) return true;
3826
+ return looksLikeCardNumber(text);
3827
+ }
3828
+ var MUTATION = 0;
3829
+ var INPUT = 5;
3830
+ var ELEMENT_NODE = 2;
3831
+ var TEXT_NODE = 3;
3832
+ var FIELD_TAGS = /* @__PURE__ */ new Set(["input", "textarea", "select"]);
3833
+ function maskTextChildren(node) {
3834
+ var _a;
3835
+ for (const child of (_a = node.childNodes) != null ? _a : []) {
3836
+ if (child.type === TEXT_NODE && typeof child.textContent === "string") {
3837
+ child.textContent = child.textContent.replace(/\S/g, "*");
3838
+ }
3839
+ maskTextChildren(child);
3840
+ }
3841
+ }
3842
+ function scrubTree(node, policy, masked) {
3843
+ var _a, _b;
3844
+ if (!node) return;
3845
+ const attributes = (_a = node.attributes) != null ? _a : {};
3846
+ const inheritedMask = masked || hasClass(attributes, policy.maskClass);
3847
+ if (node.type === ELEMENT_NODE && node.tagName && FIELD_TAGS.has(node.tagName.toLowerCase())) {
3848
+ const value = attributes.value;
3849
+ const shouldMask = policy.maskEveryField || inheritedMask || isSensitiveSerialised(attributes) || typeof value === "string" && looksLikeCardNumber(value);
3850
+ if (shouldMask) {
3851
+ if (typeof value === "string" && value !== "") attributes.value = maskValue(value);
3852
+ if (node.tagName.toLowerCase() === "textarea") maskTextChildren(node);
3853
+ }
3854
+ }
3855
+ for (const child of (_b = node.childNodes) != null ? _b : []) scrubTree(child, policy, inheritedMask);
3856
+ }
3857
+ function scrubEvent(event, getNode, policy) {
3858
+ var _a, _b, _c, _d, _e;
3859
+ if (!event.data) return;
3860
+ if (event.type === 2) {
3861
+ scrubTree(event.data.node, policy, false);
3862
+ return;
3863
+ }
3864
+ if (event.type !== 3) return;
3865
+ const data = event.data;
3866
+ if (data.source === INPUT) {
3867
+ if (typeof data.text !== "string" || data.text === "") return;
3868
+ const node = typeof data.id === "number" ? getNode(data.id) : null;
3869
+ const type = ((_b = (_a = asElement(node)) == null ? void 0 : _a.type) != null ? _b : "").toLowerCase();
3870
+ if (type === "checkbox" || type === "radio") return;
3871
+ if (shouldMaskValue(data.text, node, policy)) data.text = maskValue(data.text);
3872
+ return;
3873
+ }
3874
+ if (data.source !== MUTATION) return;
3875
+ for (const entry of (_c = data.attributes) != null ? _c : []) {
3876
+ const value = (_d = entry == null ? void 0 : entry.attributes) == null ? void 0 : _d.value;
3877
+ if (typeof value !== "string" || value === "") continue;
3878
+ const node = typeof entry.id === "number" ? getNode(entry.id) : null;
3879
+ if (shouldMaskValue(value, node, policy)) entry.attributes.value = maskValue(value);
3880
+ }
3881
+ for (const add of (_e = data.adds) != null ? _e : []) {
3882
+ const parent = typeof (add == null ? void 0 : add.parentId) === "number" ? getNode(add.parentId) : null;
3883
+ scrubTree(add == null ? void 0 : add.node, policy, isMarkedByCustomer(parent, policy.maskClass));
3884
+ }
3885
+ }
3886
+
3714
3887
  // src/sampling.ts
3715
3888
  function hashToUnitInterval(input) {
3716
3889
  let hash = 2166136261;
@@ -3806,7 +3979,14 @@ var Transport = class {
3806
3979
  seq: this.seq,
3807
3980
  events,
3808
3981
  ...this.pendingMeta ? { meta: this.pendingMeta } : {},
3809
- ...Object.keys(this.flags).length > 0 ? { flags: this.flags } : {}
3982
+ ...Object.keys(this.flags).length > 0 ? { flags: this.flags } : {},
3983
+ // This device's clock as the request leaves, on the same clock as the
3984
+ // event timestamps. Ingest compares it with its own to catch a device
3985
+ // that runs ahead — a test phone three weeks fast would otherwise list
3986
+ // its sessions as starting "in 3 weeks". Stamped here, when the body is
3987
+ // built, so a retry or a queue flushed after a spell offline carries
3988
+ // the time it was actually sent rather than when it was recorded.
3989
+ sentAt: Date.now()
3810
3990
  });
3811
3991
  }
3812
3992
  /** Sends whatever is buffered. Safe to call when there is nothing to send. */
@@ -4007,47 +4187,37 @@ function reserveSeq(session, next) {
4007
4187
  function touchSession(session, now = Date.now()) {
4008
4188
  session.set(SESSION_TS_KEY, String(now));
4009
4189
  }
4190
+ function forgetVisitor(local, session) {
4191
+ local.remove(VISITOR_KEY);
4192
+ for (const key of [SESSION_KEY, SESSION_TS_KEY, SEQ_KEY, PAGE_KEY, COOLDOWN_KEY]) session.remove(key);
4193
+ }
4010
4194
 
4011
4195
  // src/recorder.ts
4012
4196
  var RAGE_CLICK_COUNT = 3;
4013
4197
  var RAGE_CLICK_WINDOW_MS = 1e3;
4014
4198
  var RAGE_CLICK_RADIUS_PX = 30;
4015
4199
  var MAX_PENDING_EVENTS = 32;
4200
+ var EDITABLE_SELECTOR = '[contenteditable]:not([contenteditable="false"])';
4016
4201
  function createRecorder(rawOptions) {
4017
4202
  const options = resolveOptions(rawOptions);
4018
4203
  const local = safeStorage(() => globalThis.localStorage);
4019
4204
  const session = safeStorage(() => globalThis.sessionStorage);
4020
- const identity = resolveIdentity(local, session);
4021
4205
  let status = "idle";
4206
+ let identity;
4207
+ let transport;
4022
4208
  let stopRecording;
4023
- let pageCount = identity.pageCount;
4024
- if (!isSampledIn(identity.visitorId, options.sampleRate)) {
4025
- return inertHandle("sampled-out", identity.sessionId);
4026
- }
4027
- if (inCooldown(session)) {
4028
- if (options.debug) console.warn("[anyreplay] not recording: ingest refused a new session recently");
4029
- return inertHandle("stopped", identity.sessionId);
4030
- }
4031
- const transport = new Transport(
4032
- options,
4033
- {
4034
- projectKey: options.projectKey,
4035
- sessionId: identity.sessionId,
4036
- visitorId: identity.visitorId,
4037
- initialSeq: identity.nextSeq
4038
- },
4039
- {
4040
- onSeqReserved: (next) => reserveSeq(session, next),
4041
- onStopped: (reason) => {
4042
- status = "stopped";
4043
- stopRecording == null ? void 0 : stopRecording();
4044
- stopRecording = void 0;
4045
- if (reason === "rejected_402") startCooldown(session);
4046
- if (options.debug) console.warn(`[anyreplay] recording stopped: ${reason}`);
4047
- }
4048
- }
4049
- );
4050
- const collectMeta = () => ({
4209
+ let pageCount = 0;
4210
+ let pendingTraits;
4211
+ const sendIdentity = (who, traits) => {
4212
+ void fetch(`${options.ingestUrl}/v1/ingest/identify`, {
4213
+ method: "POST",
4214
+ headers: { "Content-Type": "application/json" },
4215
+ credentials: "omit",
4216
+ body: JSON.stringify({ projectKey: options.projectKey, sessionId: who.sessionId, ...traits })
4217
+ }).catch(() => {
4218
+ });
4219
+ };
4220
+ const collectMeta = (who) => ({
4051
4221
  startedAt: Date.now(),
4052
4222
  url: safeLocation(),
4053
4223
  lang: typeof navigator !== "undefined" ? navigator.language : void 0,
@@ -4057,10 +4227,10 @@ function createRecorder(rawOptions) {
4057
4227
  referrer: typeof document !== "undefined" ? document.referrer || void 0 : void 0,
4058
4228
  // The cap is a decision about starting a session, so it goes only where
4059
4229
  // ingest makes that decision: chunk 0. A resumed page starts later.
4060
- ...options.maxSessionsPerMonth !== void 0 && identity.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
4230
+ ...options.maxSessionsPerMonth !== void 0 && who.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
4061
4231
  });
4062
4232
  const recentClicks = [];
4063
- const noteInteraction = (event) => {
4233
+ const noteInteraction = (event, channel) => {
4064
4234
  var _a;
4065
4235
  const data = event.data;
4066
4236
  if (event.type !== 3 || (data == null ? void 0 : data.source) !== 2 || typeof data.x !== "number") return;
@@ -4072,7 +4242,7 @@ function createRecorder(rawOptions) {
4072
4242
  const clustered = recentClicks.every(
4073
4243
  (c) => Math.abs(c.x - first.x) < RAGE_CLICK_RADIUS_PX && Math.abs(c.y - first.y) < RAGE_CLICK_RADIUS_PX
4074
4244
  );
4075
- if (clustered) transport.mergeFlags({ hasRageClick: true });
4245
+ if (clustered) channel.mergeFlags({ hasRageClick: true });
4076
4246
  }
4077
4247
  };
4078
4248
  const pending = [];
@@ -4100,7 +4270,7 @@ function createRecorder(rawOptions) {
4100
4270
  }
4101
4271
  };
4102
4272
  const emitCustom = (tag, payload) => {
4103
- if (status === "stopped") return;
4273
+ if (status === "stopped" || status === "sampled-out") return;
4104
4274
  if (status === "recording") {
4105
4275
  try {
4106
4276
  record.addCustomEvent(tag, payload);
@@ -4114,63 +4284,104 @@ function createRecorder(rawOptions) {
4114
4284
  }
4115
4285
  pending.push({ tag, payload });
4116
4286
  };
4117
- let lastUrl = safeLocation();
4118
- const noteNavigation = () => {
4287
+ let lastUrl;
4288
+ const noteNavigation = (channel) => {
4119
4289
  const url = safeLocation();
4120
4290
  if (!url || url === lastUrl) return;
4121
4291
  lastUrl = url;
4122
4292
  pageCount += 1;
4123
4293
  notePageCount(session, pageCount);
4124
- transport.mergeFlags({ pageCount });
4294
+ channel.mergeFlags({ pageCount });
4125
4295
  emitCustom(NAVIGATE_TAG, { url, title: typeof document !== "undefined" ? document.title : "" });
4126
4296
  };
4127
4297
  const begin = () => {
4128
- if (status === "recording" || status === "stopped") return;
4298
+ if (status !== "idle" && status !== "awaiting-consent") return;
4299
+ const who = resolveIdentity(local, session);
4300
+ identity = who;
4301
+ pageCount = who.pageCount;
4302
+ if (!isSampledIn(who.visitorId, options.sampleRate)) {
4303
+ status = "sampled-out";
4304
+ return;
4305
+ }
4306
+ if (inCooldown(session)) {
4307
+ status = "stopped";
4308
+ if (options.debug) console.warn("[anyreplay] not recording: ingest refused a new session recently");
4309
+ return;
4310
+ }
4311
+ const channel = new Transport(
4312
+ options,
4313
+ {
4314
+ projectKey: options.projectKey,
4315
+ sessionId: who.sessionId,
4316
+ visitorId: who.visitorId,
4317
+ initialSeq: who.nextSeq
4318
+ },
4319
+ {
4320
+ onSeqReserved: (next) => reserveSeq(session, next),
4321
+ onStopped: (reason) => {
4322
+ status = "stopped";
4323
+ stopRecording == null ? void 0 : stopRecording();
4324
+ stopRecording = void 0;
4325
+ if (reason === "rejected_402") startCooldown(session);
4326
+ if (options.debug) console.warn(`[anyreplay] recording stopped: ${reason}`);
4327
+ }
4328
+ }
4329
+ );
4330
+ transport = channel;
4129
4331
  status = "recording";
4130
- transport.setMeta(collectMeta());
4131
- transport.mergeFlags({ pageCount });
4132
- transport.start();
4332
+ lastUrl = safeLocation();
4333
+ channel.setMeta(collectMeta(who));
4334
+ channel.mergeFlags({ pageCount });
4335
+ channel.start();
4336
+ const policy = {
4337
+ maskEveryField: options.maskAllInputs || options.maskAllTyping,
4338
+ maskClass: options.maskClass
4339
+ };
4133
4340
  stopRecording = record({
4134
4341
  emit: (event) => {
4135
4342
  const typed = event;
4136
- noteInteraction(typed);
4343
+ scrubEvent(typed, (id) => record.mirror.getNode(id), policy);
4344
+ noteInteraction(typed, channel);
4137
4345
  touchSession(session, typed.timestamp);
4138
- transport.push(typed);
4346
+ channel.push(typed);
4139
4347
  drainPending();
4140
4348
  },
4141
- maskAllInputs: options.maskAllInputs,
4349
+ maskInputOptions: policy.maskEveryField ? MASK_EVERY_FIELD : MASK_SENSITIVE_FIELDS,
4142
4350
  maskTextClass: options.maskClass,
4351
+ maskTextSelector: options.maskAllTyping ? EDITABLE_SELECTOR : void 0,
4143
4352
  blockClass: options.blockClass,
4144
- // Passwords are never recorded, regardless of configuration.
4145
- maskInputOptions: { password: true, email: true, tel: true },
4146
4353
  // Canvas capture is expensive and off unless a customer opts in later.
4147
4354
  recordCanvas: false,
4148
4355
  collectFonts: false
4149
4356
  });
4150
4357
  drainPending();
4151
- attachLifecycleHooks();
4358
+ if (pendingTraits) {
4359
+ sendIdentity(who, pendingTraits);
4360
+ pendingTraits = void 0;
4361
+ }
4362
+ attachLifecycleHooks(channel);
4152
4363
  };
4153
- const attachLifecycleHooks = () => {
4364
+ const attachLifecycleHooks = (channel) => {
4154
4365
  if (typeof window === "undefined") return;
4155
4366
  window.addEventListener("pagehide", () => {
4156
- transport.flushWithBeacon();
4367
+ channel.flushWithBeacon();
4157
4368
  }, { capture: true });
4158
4369
  document.addEventListener("visibilitychange", () => {
4159
- if (document.visibilityState === "hidden") transport.flushWithBeacon();
4370
+ if (document.visibilityState === "hidden") channel.flushWithBeacon();
4160
4371
  });
4161
- window.addEventListener("error", () => transport.mergeFlags({ hasError: true }));
4162
- window.addEventListener("unhandledrejection", () => transport.mergeFlags({ hasError: true }));
4372
+ window.addEventListener("error", () => channel.mergeFlags({ hasError: true }));
4373
+ window.addEventListener("unhandledrejection", () => channel.mergeFlags({ hasError: true }));
4163
4374
  const patch2 = (method) => {
4164
4375
  const original = history[method].bind(history);
4165
4376
  history[method] = function patched(...args) {
4166
4377
  const result = original(...args);
4167
- noteNavigation();
4378
+ noteNavigation(channel);
4168
4379
  return result;
4169
4380
  };
4170
4381
  };
4171
4382
  patch2("pushState");
4172
4383
  patch2("replaceState");
4173
- window.addEventListener("popstate", noteNavigation);
4384
+ window.addEventListener("popstate", () => noteNavigation(channel));
4174
4385
  };
4175
4386
  if (options.requireConsent) {
4176
4387
  status = "awaiting-consent";
@@ -4179,24 +4390,28 @@ function createRecorder(rawOptions) {
4179
4390
  }
4180
4391
  return {
4181
4392
  status: () => status,
4182
- sessionId: () => identity.sessionId,
4393
+ sessionId: () => {
4394
+ var _a;
4395
+ return (_a = identity == null ? void 0 : identity.sessionId) != null ? _a : null;
4396
+ },
4183
4397
  consent: (granted) => {
4184
- if (granted) begin();
4185
- else transport.stop("consent_withdrawn");
4398
+ if (granted) {
4399
+ begin();
4400
+ return;
4401
+ }
4402
+ transport == null ? void 0 : transport.stop("consent_withdrawn");
4403
+ if (status === "idle" || status === "awaiting-consent") status = "stopped";
4404
+ pendingTraits = void 0;
4405
+ forgetVisitor(local, session);
4186
4406
  },
4187
4407
  identify: (traits) => {
4188
4408
  if (!traits.userId && !traits.email) return;
4189
- void fetch(`${options.ingestUrl}/v1/ingest/identify`, {
4190
- method: "POST",
4191
- headers: { "Content-Type": "application/json" },
4192
- credentials: "omit",
4193
- body: JSON.stringify({
4194
- projectKey: options.projectKey,
4195
- sessionId: identity.sessionId,
4196
- ...traits
4197
- })
4198
- }).catch(() => {
4199
- });
4409
+ if (identity && transport) {
4410
+ sendIdentity(identity, traits);
4411
+ return;
4412
+ }
4413
+ if (status === "idle" || status === "awaiting-consent") pendingTraits = traits;
4414
+ else if (options.debug) console.warn("[anyreplay] identify() ignored: not recording");
4200
4415
  },
4201
4416
  track: (name, properties) => {
4202
4417
  const payload = validateTrack(name, properties);
@@ -4206,8 +4421,15 @@ function createRecorder(rawOptions) {
4206
4421
  }
4207
4422
  emitCustom(TRACK_TAG, payload);
4208
4423
  },
4209
- stop: () => transport.stop("stopped_by_page"),
4210
- flush: () => transport.flush()
4424
+ stop: () => {
4425
+ transport == null ? void 0 : transport.stop("stopped_by_page");
4426
+ if (status === "idle" || status === "awaiting-consent") status = "stopped";
4427
+ pendingTraits = void 0;
4428
+ },
4429
+ flush: () => {
4430
+ var _a;
4431
+ return (_a = transport == null ? void 0 : transport.flush()) != null ? _a : Promise.resolve();
4432
+ }
4211
4433
  };
4212
4434
  }
4213
4435
  function safeLocation() {
@@ -4217,22 +4439,6 @@ function safeLocation() {
4217
4439
  return void 0;
4218
4440
  }
4219
4441
  }
4220
- function inertHandle(status, sessionId) {
4221
- return {
4222
- status: () => status,
4223
- sessionId: () => sessionId,
4224
- consent: () => {
4225
- },
4226
- identify: () => {
4227
- },
4228
- track: () => {
4229
- },
4230
- stop: () => {
4231
- },
4232
- flush: async () => {
4233
- }
4234
- };
4235
- }
4236
4442
  export {
4237
4443
  ConfigError,
4238
4444
  DEFAULTS,
@@ -4242,6 +4448,7 @@ export {
4242
4448
  TRACK_TAG,
4243
4449
  Transport,
4244
4450
  createRecorder,
4451
+ forgetVisitor,
4245
4452
  hashToUnitInterval,
4246
4453
  isSampledIn,
4247
4454
  resolveIdentity,