@faststats/web 0.10.3 → 0.12.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.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ExtensionHook, i as ExtensionHandler, n as AnalyticsExtensionContext, o as ExtensionHooks, r as ExtensionCleanup, t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
1
+ import { a as ExtensionHook, i as ExtensionHandler, n as AnalyticsExtensionContext, o as ExtensionHooks, r as ExtensionCleanup, t as AnalyticsExtension } from "./chunks/extensions-BfubKIzk.js";
2
2
  //#region src/analytics.d.ts
3
3
  type ConsentMode = "anonymous" | "granted" | "denied";
4
4
  interface IdentifyOptions {
@@ -32,7 +32,7 @@ export declare class WebAnalytics {
32
32
  private readonly baseUrl;
33
33
  private readonly debug;
34
34
  private readonly cleanup;
35
- private started;
35
+ private state;
36
36
  private heartbeatTimer;
37
37
  private extensions;
38
38
  private pageKey;
@@ -53,6 +53,8 @@ export declare class WebAnalytics {
53
53
  private canTrack;
54
54
  private isCookieless;
55
55
  private isActive;
56
+ private session;
57
+ private context;
56
58
  private activate;
57
59
  private startExtensions;
58
60
  private stopExtensions;
package/dist/index.js CHANGED
@@ -1,22 +1,24 @@
1
- import { i as resolveBaseUrl, n as URLS, r as getPageContext, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
- import { a as resetSession, n as createId, o as touchActivity, r as getSessionContext, t as sendData } from "./chunks/send-data-DAVUdfNm.js";
3
- import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-BgGtAQdp.js";
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as sendData } from "./chunks/send-data-DYZZjeM-.js";
2
+ import { a as touchActivity, i as resetSession, n as getSessionContext, t as createId } from "./chunks/session-manager-CFQEWbgo.js";
3
+ import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-CpmDmpDv.js";
4
4
  //#region src/extensions.ts
5
5
  var ExtensionHost = class {
6
- runtime;
6
+ context;
7
+ log;
7
8
  handlers = /* @__PURE__ */ new Map();
8
9
  cleanups = [];
9
10
  stopped = false;
10
- constructor(extensions, runtime) {
11
- this.runtime = runtime;
11
+ constructor(extensions, context, log) {
12
+ this.context = context;
13
+ this.log = log;
12
14
  for (const extension of extensions) this.setup(extension);
13
15
  }
14
16
  emit(hook, event) {
15
17
  if (this.stopped) return;
16
18
  for (const handler of this.handlers.get(hook) ?? []) try {
17
- handler(event)?.catch((error) => this.runtime.log(`${hook} failed`, error));
19
+ handler(event)?.catch((error) => this.log(`${hook} failed`, error));
18
20
  } catch (error) {
19
- this.runtime.log(`${hook} failed`, error);
21
+ this.log(`${hook} failed`, error);
20
22
  }
21
23
  }
22
24
  stop(discard = false) {
@@ -25,7 +27,7 @@ var ExtensionHost = class {
25
27
  for (const cleanup of this.cleanups.reverse()) try {
26
28
  cleanup({ discard });
27
29
  } catch (error) {
28
- this.runtime.log("cleanup failed", error);
30
+ this.log("cleanup failed", error);
29
31
  }
30
32
  this.cleanups.length = 0;
31
33
  this.handlers.clear();
@@ -35,7 +37,7 @@ var ExtensionHost = class {
35
37
  const subscriptions = [];
36
38
  try {
37
39
  const cleanup = await extension.setup({
38
- ...this.runtime,
40
+ ...this.context,
39
41
  on: (hook, handler) => {
40
42
  if (this.stopped) return () => {};
41
43
  let handlers = this.handlers.get(hook);
@@ -53,11 +55,40 @@ var ExtensionHost = class {
53
55
  else if (cleanup) this.cleanups.push(cleanup);
54
56
  } catch (error) {
55
57
  for (const off of subscriptions) off();
56
- this.runtime.log(`${extension.name} setup failed`, error);
58
+ this.log(`${extension.name} setup failed`, error);
57
59
  }
58
60
  }
59
61
  };
60
62
  //#endregion
63
+ //#region src/outbound-links.ts
64
+ const DEDUPE_MS = 500;
65
+ function outboundHref(event) {
66
+ for (const node of event.composedPath()) if (node instanceof HTMLAnchorElement && (node.protocol === "http:" || node.protocol === "https:") && node.host !== location.host) return node.href;
67
+ }
68
+ function trackOutboundLinks(report) {
69
+ let last;
70
+ const onClick = (event) => {
71
+ if (!(event instanceof MouseEvent) || event.defaultPrevented) return;
72
+ if (event.button !== (event.type === "click" ? 0 : 1)) return;
73
+ const href = outboundHref(event);
74
+ if (!href) return;
75
+ const key = `${event.button}:${event.clientX}:${event.clientY}:${href}`;
76
+ const now = Date.now();
77
+ const duplicate = last?.key === key && now - last.at <= DEDUPE_MS;
78
+ last = {
79
+ key,
80
+ at: now
81
+ };
82
+ if (!duplicate) report(sanitizeUrl(href));
83
+ };
84
+ document.addEventListener("click", onClick, true);
85
+ document.addEventListener("auxclick", onClick, true);
86
+ return () => {
87
+ document.removeEventListener("click", onClick, true);
88
+ document.removeEventListener("auxclick", onClick, true);
89
+ };
90
+ }
91
+ //#endregion
61
92
  //#region src/analytics.ts
62
93
  const navigationListeners = /* @__PURE__ */ new Set();
63
94
  const historyPatches = {};
@@ -95,12 +126,18 @@ function observeHistory(listener) {
95
126
  }
96
127
  };
97
128
  }
129
+ const RESERVED_EVENTS = /* @__PURE__ */ new Set([
130
+ "pageview",
131
+ "page_leave",
132
+ "error",
133
+ "outbound_link"
134
+ ]);
98
135
  var WebAnalytics = class {
99
136
  options;
100
137
  baseUrl;
101
138
  debug;
102
139
  cleanup = [];
103
- started = false;
140
+ state = "idle";
104
141
  heartbeatTimer = null;
105
142
  extensions = null;
106
143
  pageKey = "";
@@ -110,20 +147,20 @@ var WebAnalytics = class {
110
147
  constructor(options) {
111
148
  this.options = options;
112
149
  if (!options.siteKey.trim()) throw new Error("siteKey is required");
113
- this.baseUrl = resolveBaseUrl(options.baseUrl, ANALYTICS_BASE);
150
+ this.baseUrl = resolveBaseUrl(options.baseUrl);
114
151
  this.debug = options.debug ?? false;
115
152
  this.consentMode = options.consent ?? "granted";
116
153
  }
117
154
  start() {
118
- if (this.started !== false || typeof window === "undefined") return;
119
- this.started = true;
155
+ if (this.state !== "idle" || typeof window === "undefined") return;
156
+ this.state = "started";
120
157
  this.installBrowserLifecycle();
121
158
  if (this.canTrack()) this.activate("load");
122
159
  }
123
160
  destroy() {
124
- if (this.started === null) return;
161
+ if (this.state === "destroyed") return;
125
162
  this.leavePage();
126
- this.started = null;
163
+ this.state = "destroyed";
127
164
  this.pendingPageview = null;
128
165
  for (const cleanup of this.cleanup.splice(0).reverse()) cleanup();
129
166
  this.stopExtensions(false);
@@ -135,26 +172,22 @@ var WebAnalytics = class {
135
172
  this.leavePage();
136
173
  if (this.pageKey) this.extensions?.emit("pageChange", { url: getPageContext(this.options.trackHash).url ?? "" });
137
174
  this.pageKey = key;
175
+ this.pendingPageview = null;
138
176
  }
139
177
  if (document.visibilityState !== "visible") {
140
- this.pendingPageview = properties;
178
+ this.pendingPageview ??= properties;
141
179
  return;
142
180
  }
143
181
  this.pendingPageview = null;
144
182
  if (document.hasFocus()) touchActivity(this.options.siteKey, this.isCookieless());
145
- const session = getSessionContext(this.options.siteKey, this.isCookieless());
146
- const userId = this.getAnonymousId();
147
- if (this.visit?.session.sessionId === session.sessionId && this.visit.context.userId === (userId || void 0)) return;
183
+ const session = this.session();
184
+ const context = this.context(session);
185
+ if (this.visit?.session.sessionId === session.sessionId && this.visit.context.userId === context.userId) return;
148
186
  this.leavePage();
149
187
  this.visit = {
150
188
  id: createId(),
151
189
  session,
152
- context: {
153
- ...getPageContext(this.options.trackHash),
154
- userId: userId || void 0,
155
- sessionId: session.sessionId,
156
- windowId: session.windowId
157
- },
190
+ context,
158
191
  duration: 0,
159
192
  startedAt: null,
160
193
  reportedDuration: -1
@@ -168,7 +201,7 @@ var WebAnalytics = class {
168
201
  track(name, properties = {}) {
169
202
  if (!this.isActive()) return;
170
203
  const eventName = name.trim();
171
- if (!eventName || eventName === "pageview" || eventName === "page_leave" || eventName === "error" || eventName === "outbound_link") return;
204
+ if (!eventName || RESERVED_EVENTS.has(eventName)) return;
172
205
  this.pageview({ trigger: "session" });
173
206
  this.send(eventName, properties);
174
207
  }
@@ -181,6 +214,7 @@ var WebAnalytics = class {
181
214
  } : userOrExternalId;
182
215
  const externalId = user.id.trim();
183
216
  if (!externalId) return false;
217
+ this.extensions?.emit("identityChange", { userId: externalId });
184
218
  const clearFields = [
185
219
  "email",
186
220
  "name",
@@ -214,6 +248,7 @@ var WebAnalytics = class {
214
248
  this.leavePage();
215
249
  if (resetAnonymousIdentity) resetAnonymousId(this.options.siteKey, this.isCookieless());
216
250
  resetSession(this.options.siteKey, this.isCookieless());
251
+ this.extensions?.emit("identityChange", { userId: void 0 });
217
252
  this.pageview({ trigger: "logout" });
218
253
  }
219
254
  setConsentMode(mode) {
@@ -233,7 +268,7 @@ var WebAnalytics = class {
233
268
  if (wasTracking) {
234
269
  this.extensions?.emit("consentChange", { cookieless: willBeCookieless });
235
270
  if (wasCookieless !== willBeCookieless) this.pageview({ trigger: "consent" });
236
- } else if (this.started) this.activate("consent");
271
+ } else if (this.state === "started") this.activate("consent");
237
272
  }
238
273
  getAnonymousId() {
239
274
  return this.canTrack() ? getAnonymousId(this.options.siteKey, this.isCookieless()) : "";
@@ -249,7 +284,18 @@ var WebAnalytics = class {
249
284
  return this.options.cookieless === true || this.consentMode === "anonymous";
250
285
  }
251
286
  isActive() {
252
- return this.started === true && this.canTrack();
287
+ return this.state === "started" && this.canTrack();
288
+ }
289
+ session() {
290
+ return getSessionContext(this.options.siteKey, this.isCookieless());
291
+ }
292
+ context(session = this.session()) {
293
+ return {
294
+ ...getPageContext(this.options.trackHash),
295
+ userId: this.getAnonymousId() || void 0,
296
+ sessionId: session.sessionId,
297
+ windowId: session.windowId
298
+ };
253
299
  }
254
300
  activate(trigger) {
255
301
  this.startExtensions();
@@ -257,22 +303,17 @@ var WebAnalytics = class {
257
303
  }
258
304
  startExtensions() {
259
305
  if (this.extensions || !this.options.extensions?.length) return;
260
- const host = new ExtensionHost(this.options.extensions, {
306
+ this.extensions = new ExtensionHost(this.options.extensions, {
261
307
  siteKey: this.options.siteKey,
262
308
  baseUrl: this.baseUrl,
263
309
  debug: this.debug,
264
310
  trackHash: this.options.trackHash ?? false,
265
311
  sdkName: this.options.sdkName,
266
312
  sdkVersion: this.options.sdkVersion,
267
- isCookieless: () => this.isCookieless(),
268
- getAnonymousId: () => this.getAnonymousId(),
269
- getSession: () => getSessionContext(this.options.siteKey, this.isCookieless()),
270
- capture: (event, properties) => this.send(event, properties),
271
- log: (...details) => {
272
- if (this.debug) console.log(...details);
273
- }
313
+ isCookieless: () => this.isCookieless()
314
+ }, (...details) => {
315
+ if (this.debug) console.log(...details);
274
316
  });
275
- this.extensions = host;
276
317
  }
277
318
  stopExtensions(discard) {
278
319
  this.extensions?.stop(discard);
@@ -286,7 +327,7 @@ var WebAnalytics = class {
286
327
  this.bind(window, "blur", () => this.pauseVisit());
287
328
  this.bind(window, "popstate", this.onNavigate);
288
329
  if (this.options.trackHash) this.bind(window, "hashchange", this.onNavigate);
289
- this.cleanup.push(observeHistory(this.onNavigate));
330
+ this.cleanup.push(observeHistory(this.onNavigate), trackOutboundLinks((url) => this.send("outbound_link", { outbound_link: url })));
290
331
  }
291
332
  bind(target, type, listener) {
292
333
  target.addEventListener(type, listener);
@@ -359,21 +400,14 @@ var WebAnalytics = class {
359
400
  this.pauseVisit();
360
401
  return;
361
402
  }
403
+ const visit = this.visit;
362
404
  this.pageview({ trigger: "session" });
363
- this.checkpoint();
405
+ if (this.visit === visit) this.checkpoint();
364
406
  }, 15e3);
365
407
  }
366
408
  send(event, properties = {}, context) {
367
409
  if (!this.isActive()) return;
368
- if (!context) {
369
- const session = getSessionContext(this.options.siteKey, this.isCookieless());
370
- context = {
371
- ...getPageContext(this.options.trackHash),
372
- userId: this.getAnonymousId() || void 0,
373
- sessionId: session.sessionId,
374
- windowId: session.windowId
375
- };
376
- }
410
+ context ??= this.context();
377
411
  let data;
378
412
  try {
379
413
  data = JSON.stringify({
package/dist/replay.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- import { t as AnalyticsExtension } from "./chunks/extensions-DW7tJY0T.js";
1
+ import { t as AnalyticsExtension } from "./chunks/extensions-BfubKIzk.js";
2
2
  import { record } from "@rrweb/record";
3
3
  import { LogLevel } from "@rrweb/rrweb-plugin-console-record";
4
4
  import { NetworkRecordOptions } from "@rrweb/rrweb-plugin-network-record";
5
- //#region ../../node_modules/.bun/@rrweb+types@2.1.6/node_modules/@rrweb/types/dist/index.d.ts
5
+ //#region ../../node_modules/.bun/@rrweb+types@2.1.7/node_modules/@rrweb/types/dist/index.d.ts
6
6
  declare global {
7
7
  interface Window {
8
8
  FontFace: typeof FontFace;
@@ -58,7 +58,7 @@ export declare function sessionReplay(options?: SessionReplayExtensionOptions):
58
58
  export default class ReplayTracker {
59
59
  private readonly options;
60
60
  private readonly endpoint;
61
- private readonly compressionSupported;
61
+ private readonly compress;
62
62
  private readonly flushInterval;
63
63
  private readonly maxEvents;
64
64
  private readonly maxBatchSizeBytes;
@@ -95,7 +95,10 @@ export default class ReplayTracker {
95
95
  private readonly mutationThrottler;
96
96
  constructor(options: ReplayTrackerOptions);
97
97
  private log;
98
+ private sessionContext;
99
+ private sanitize;
98
100
  private clearFlushTimers;
101
+ private dropEvents;
99
102
  private clearQueues;
100
103
  private finalizeSession;
101
104
  setCookielessMode(cookieless: boolean): void;
@@ -112,7 +115,6 @@ export default class ReplayTracker {
112
115
  private beginReplayStream;
113
116
  private resetSessionTiming;
114
117
  private onEvent;
115
- private onUnload;
116
118
  private requestTerminalFlush;
117
119
  private hasReachedMinLength;
118
120
  private requestFlush;