@anyreplay/browser 0.1.1 → 0.1.2

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
@@ -30,6 +30,7 @@ __export(src_exports, {
30
30
  TRACK_TAG: () => TRACK_TAG,
31
31
  Transport: () => Transport,
32
32
  createRecorder: () => createRecorder,
33
+ forgetVisitor: () => forgetVisitor,
33
34
  hashToUnitInterval: () => hashToUnitInterval,
34
35
  isSampledIn: () => isSampledIn,
35
36
  resolveIdentity: () => resolveIdentity,
@@ -4039,6 +4040,10 @@ function reserveSeq(session, next) {
4039
4040
  function touchSession(session, now = Date.now()) {
4040
4041
  session.set(SESSION_TS_KEY, String(now));
4041
4042
  }
4043
+ function forgetVisitor(local, session) {
4044
+ local.remove(VISITOR_KEY);
4045
+ for (const key of [SESSION_KEY, SESSION_TS_KEY, SEQ_KEY, PAGE_KEY, COOLDOWN_KEY]) session.remove(key);
4046
+ }
4042
4047
 
4043
4048
  // src/recorder.ts
4044
4049
  var RAGE_CLICK_COUNT = 3;
@@ -4049,37 +4054,22 @@ function createRecorder(rawOptions) {
4049
4054
  const options = resolveOptions(rawOptions);
4050
4055
  const local = safeStorage(() => globalThis.localStorage);
4051
4056
  const session = safeStorage(() => globalThis.sessionStorage);
4052
- const identity = resolveIdentity(local, session);
4053
4057
  let status = "idle";
4058
+ let identity;
4059
+ let transport;
4054
4060
  let stopRecording;
4055
- let pageCount = identity.pageCount;
4056
- if (!isSampledIn(identity.visitorId, options.sampleRate)) {
4057
- return inertHandle("sampled-out", identity.sessionId);
4058
- }
4059
- if (inCooldown(session)) {
4060
- if (options.debug) console.warn("[anyreplay] not recording: ingest refused a new session recently");
4061
- return inertHandle("stopped", identity.sessionId);
4062
- }
4063
- const transport = new Transport(
4064
- options,
4065
- {
4066
- projectKey: options.projectKey,
4067
- sessionId: identity.sessionId,
4068
- visitorId: identity.visitorId,
4069
- initialSeq: identity.nextSeq
4070
- },
4071
- {
4072
- onSeqReserved: (next) => reserveSeq(session, next),
4073
- onStopped: (reason) => {
4074
- status = "stopped";
4075
- stopRecording == null ? void 0 : stopRecording();
4076
- stopRecording = void 0;
4077
- if (reason === "rejected_402") startCooldown(session);
4078
- if (options.debug) console.warn(`[anyreplay] recording stopped: ${reason}`);
4079
- }
4080
- }
4081
- );
4082
- const collectMeta = () => ({
4061
+ let pageCount = 0;
4062
+ let pendingTraits;
4063
+ const sendIdentity = (who, traits) => {
4064
+ void fetch(`${options.ingestUrl}/v1/ingest/identify`, {
4065
+ method: "POST",
4066
+ headers: { "Content-Type": "application/json" },
4067
+ credentials: "omit",
4068
+ body: JSON.stringify({ projectKey: options.projectKey, sessionId: who.sessionId, ...traits })
4069
+ }).catch(() => {
4070
+ });
4071
+ };
4072
+ const collectMeta = (who) => ({
4083
4073
  startedAt: Date.now(),
4084
4074
  url: safeLocation(),
4085
4075
  lang: typeof navigator !== "undefined" ? navigator.language : void 0,
@@ -4089,10 +4079,10 @@ function createRecorder(rawOptions) {
4089
4079
  referrer: typeof document !== "undefined" ? document.referrer || void 0 : void 0,
4090
4080
  // The cap is a decision about starting a session, so it goes only where
4091
4081
  // ingest makes that decision: chunk 0. A resumed page starts later.
4092
- ...options.maxSessionsPerMonth !== void 0 && identity.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
4082
+ ...options.maxSessionsPerMonth !== void 0 && who.nextSeq === 0 ? { sessionCap: options.maxSessionsPerMonth } : {}
4093
4083
  });
4094
4084
  const recentClicks = [];
4095
- const noteInteraction = (event) => {
4085
+ const noteInteraction = (event, channel) => {
4096
4086
  var _a;
4097
4087
  const data = event.data;
4098
4088
  if (event.type !== 3 || (data == null ? void 0 : data.source) !== 2 || typeof data.x !== "number") return;
@@ -4104,7 +4094,7 @@ function createRecorder(rawOptions) {
4104
4094
  const clustered = recentClicks.every(
4105
4095
  (c) => Math.abs(c.x - first.x) < RAGE_CLICK_RADIUS_PX && Math.abs(c.y - first.y) < RAGE_CLICK_RADIUS_PX
4106
4096
  );
4107
- if (clustered) transport.mergeFlags({ hasRageClick: true });
4097
+ if (clustered) channel.mergeFlags({ hasRageClick: true });
4108
4098
  }
4109
4099
  };
4110
4100
  const pending = [];
@@ -4132,7 +4122,7 @@ function createRecorder(rawOptions) {
4132
4122
  }
4133
4123
  };
4134
4124
  const emitCustom = (tag, payload) => {
4135
- if (status === "stopped") return;
4125
+ if (status === "stopped" || status === "sampled-out") return;
4136
4126
  if (status === "recording") {
4137
4127
  try {
4138
4128
  record.addCustomEvent(tag, payload);
@@ -4146,28 +4136,61 @@ function createRecorder(rawOptions) {
4146
4136
  }
4147
4137
  pending.push({ tag, payload });
4148
4138
  };
4149
- let lastUrl = safeLocation();
4150
- const noteNavigation = () => {
4139
+ let lastUrl;
4140
+ const noteNavigation = (channel) => {
4151
4141
  const url = safeLocation();
4152
4142
  if (!url || url === lastUrl) return;
4153
4143
  lastUrl = url;
4154
4144
  pageCount += 1;
4155
4145
  notePageCount(session, pageCount);
4156
- transport.mergeFlags({ pageCount });
4146
+ channel.mergeFlags({ pageCount });
4157
4147
  emitCustom(NAVIGATE_TAG, { url, title: typeof document !== "undefined" ? document.title : "" });
4158
4148
  };
4159
4149
  const begin = () => {
4160
- if (status === "recording" || status === "stopped") return;
4150
+ if (status !== "idle" && status !== "awaiting-consent") return;
4151
+ const who = resolveIdentity(local, session);
4152
+ identity = who;
4153
+ pageCount = who.pageCount;
4154
+ if (!isSampledIn(who.visitorId, options.sampleRate)) {
4155
+ status = "sampled-out";
4156
+ return;
4157
+ }
4158
+ if (inCooldown(session)) {
4159
+ status = "stopped";
4160
+ if (options.debug) console.warn("[anyreplay] not recording: ingest refused a new session recently");
4161
+ return;
4162
+ }
4163
+ const channel = new Transport(
4164
+ options,
4165
+ {
4166
+ projectKey: options.projectKey,
4167
+ sessionId: who.sessionId,
4168
+ visitorId: who.visitorId,
4169
+ initialSeq: who.nextSeq
4170
+ },
4171
+ {
4172
+ onSeqReserved: (next) => reserveSeq(session, next),
4173
+ onStopped: (reason) => {
4174
+ status = "stopped";
4175
+ stopRecording == null ? void 0 : stopRecording();
4176
+ stopRecording = void 0;
4177
+ if (reason === "rejected_402") startCooldown(session);
4178
+ if (options.debug) console.warn(`[anyreplay] recording stopped: ${reason}`);
4179
+ }
4180
+ }
4181
+ );
4182
+ transport = channel;
4161
4183
  status = "recording";
4162
- transport.setMeta(collectMeta());
4163
- transport.mergeFlags({ pageCount });
4164
- transport.start();
4184
+ lastUrl = safeLocation();
4185
+ channel.setMeta(collectMeta(who));
4186
+ channel.mergeFlags({ pageCount });
4187
+ channel.start();
4165
4188
  stopRecording = record({
4166
4189
  emit: (event) => {
4167
4190
  const typed = event;
4168
- noteInteraction(typed);
4191
+ noteInteraction(typed, channel);
4169
4192
  touchSession(session, typed.timestamp);
4170
- transport.push(typed);
4193
+ channel.push(typed);
4171
4194
  drainPending();
4172
4195
  },
4173
4196
  maskAllInputs: options.maskAllInputs,
@@ -4180,29 +4203,33 @@ function createRecorder(rawOptions) {
4180
4203
  collectFonts: false
4181
4204
  });
4182
4205
  drainPending();
4183
- attachLifecycleHooks();
4206
+ if (pendingTraits) {
4207
+ sendIdentity(who, pendingTraits);
4208
+ pendingTraits = void 0;
4209
+ }
4210
+ attachLifecycleHooks(channel);
4184
4211
  };
4185
- const attachLifecycleHooks = () => {
4212
+ const attachLifecycleHooks = (channel) => {
4186
4213
  if (typeof window === "undefined") return;
4187
4214
  window.addEventListener("pagehide", () => {
4188
- transport.flushWithBeacon();
4215
+ channel.flushWithBeacon();
4189
4216
  }, { capture: true });
4190
4217
  document.addEventListener("visibilitychange", () => {
4191
- if (document.visibilityState === "hidden") transport.flushWithBeacon();
4218
+ if (document.visibilityState === "hidden") channel.flushWithBeacon();
4192
4219
  });
4193
- window.addEventListener("error", () => transport.mergeFlags({ hasError: true }));
4194
- window.addEventListener("unhandledrejection", () => transport.mergeFlags({ hasError: true }));
4220
+ window.addEventListener("error", () => channel.mergeFlags({ hasError: true }));
4221
+ window.addEventListener("unhandledrejection", () => channel.mergeFlags({ hasError: true }));
4195
4222
  const patch2 = (method) => {
4196
4223
  const original = history[method].bind(history);
4197
4224
  history[method] = function patched(...args) {
4198
4225
  const result = original(...args);
4199
- noteNavigation();
4226
+ noteNavigation(channel);
4200
4227
  return result;
4201
4228
  };
4202
4229
  };
4203
4230
  patch2("pushState");
4204
4231
  patch2("replaceState");
4205
- window.addEventListener("popstate", noteNavigation);
4232
+ window.addEventListener("popstate", () => noteNavigation(channel));
4206
4233
  };
4207
4234
  if (options.requireConsent) {
4208
4235
  status = "awaiting-consent";
@@ -4211,24 +4238,28 @@ function createRecorder(rawOptions) {
4211
4238
  }
4212
4239
  return {
4213
4240
  status: () => status,
4214
- sessionId: () => identity.sessionId,
4241
+ sessionId: () => {
4242
+ var _a;
4243
+ return (_a = identity == null ? void 0 : identity.sessionId) != null ? _a : null;
4244
+ },
4215
4245
  consent: (granted) => {
4216
- if (granted) begin();
4217
- else transport.stop("consent_withdrawn");
4246
+ if (granted) {
4247
+ begin();
4248
+ return;
4249
+ }
4250
+ transport == null ? void 0 : transport.stop("consent_withdrawn");
4251
+ if (status === "idle" || status === "awaiting-consent") status = "stopped";
4252
+ pendingTraits = void 0;
4253
+ forgetVisitor(local, session);
4218
4254
  },
4219
4255
  identify: (traits) => {
4220
4256
  if (!traits.userId && !traits.email) return;
4221
- void fetch(`${options.ingestUrl}/v1/ingest/identify`, {
4222
- method: "POST",
4223
- headers: { "Content-Type": "application/json" },
4224
- credentials: "omit",
4225
- body: JSON.stringify({
4226
- projectKey: options.projectKey,
4227
- sessionId: identity.sessionId,
4228
- ...traits
4229
- })
4230
- }).catch(() => {
4231
- });
4257
+ if (identity && transport) {
4258
+ sendIdentity(identity, traits);
4259
+ return;
4260
+ }
4261
+ if (status === "idle" || status === "awaiting-consent") pendingTraits = traits;
4262
+ else if (options.debug) console.warn("[anyreplay] identify() ignored: not recording");
4232
4263
  },
4233
4264
  track: (name, properties) => {
4234
4265
  const payload = validateTrack(name, properties);
@@ -4238,8 +4269,15 @@ function createRecorder(rawOptions) {
4238
4269
  }
4239
4270
  emitCustom(TRACK_TAG, payload);
4240
4271
  },
4241
- stop: () => transport.stop("stopped_by_page"),
4242
- flush: () => transport.flush()
4272
+ stop: () => {
4273
+ transport == null ? void 0 : transport.stop("stopped_by_page");
4274
+ if (status === "idle" || status === "awaiting-consent") status = "stopped";
4275
+ pendingTraits = void 0;
4276
+ },
4277
+ flush: () => {
4278
+ var _a;
4279
+ return (_a = transport == null ? void 0 : transport.flush()) != null ? _a : Promise.resolve();
4280
+ }
4243
4281
  };
4244
4282
  }
4245
4283
  function safeLocation() {
@@ -4249,22 +4287,6 @@ function safeLocation() {
4249
4287
  return void 0;
4250
4288
  }
4251
4289
  }
4252
- function inertHandle(status, sessionId) {
4253
- return {
4254
- status: () => status,
4255
- sessionId: () => sessionId,
4256
- consent: () => {
4257
- },
4258
- identify: () => {
4259
- },
4260
- track: () => {
4261
- },
4262
- stop: () => {
4263
- },
4264
- flush: async () => {
4265
- }
4266
- };
4267
- }
4268
4290
  // Annotate the CommonJS export names for ESM import in node:
4269
4291
  0 && (module.exports = {
4270
4292
  ConfigError,
@@ -4275,6 +4297,7 @@ function inertHandle(status, sessionId) {
4275
4297
  TRACK_TAG,
4276
4298
  Transport,
4277
4299
  createRecorder,
4300
+ forgetVisitor,
4278
4301
  hashToUnitInterval,
4279
4302
  isSampledIn,
4280
4303
  resolveIdentity,