@faststats/web 0.7.0 → 0.8.1

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
@@ -1,92 +1,108 @@
1
- import { a as resolveBaseUrl, i as getPageContext, n as FEATURE_FLAGS_BASE, o as sanitizeUrl, r as URLS, t as ANALYTICS_BASE } from "./chunks/api-urls-Dp2XNjRF.js";
2
- import { c as touchActivity, l as getStorageItem, o as resetSession, r as getSessionContext, s as setCookielessMode, t as sendData } from "./chunks/send-data-DR1JUXka.js";
3
- import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-BvRNbiwA.js";
4
- //#region src/outbound-links.ts
5
- const DEDUPE_MS = 500;
6
- var OutboundLinkTracker = class {
7
- lastActivation = null;
8
- getHref(event) {
9
- if (!(event instanceof MouseEvent) || !isEligibleActivation(event)) return null;
10
- const anchor = findOutboundAnchor(event);
11
- if (!anchor || this.isDuplicate(event, anchor.href)) return null;
12
- return anchor.href;
1
+ import { a as sanitizeUrl, i as resolveBaseUrl, n as URLS, r as getPageContext, t as ANALYTICS_BASE } from "./chunks/api-urls-DWk43fu6.js";
2
+ import { a as resetSession, o as touchActivity, r as getSessionContext, t as sendData } from "./chunks/send-data-DFZnsaL2.js";
3
+ import { n as resetAnonymousId, t as getAnonymousId } from "./chunks/identifiers-KgQDX74Q.js";
4
+ //#region src/extensions.ts
5
+ var ExtensionHost = class {
6
+ runtime;
7
+ handlers = /* @__PURE__ */ new Map();
8
+ cleanups = [];
9
+ stopped = false;
10
+ constructor(extensions, runtime) {
11
+ this.runtime = runtime;
12
+ for (const extension of extensions) this.setup(extension);
13
+ }
14
+ emit(hook, event) {
15
+ if (this.stopped) return;
16
+ for (const handler of this.handlers.get(hook) ?? []) try {
17
+ handler(event)?.catch((error) => this.runtime.log(`${hook} failed`, error));
18
+ } catch (error) {
19
+ this.runtime.log(`${hook} failed`, error);
20
+ }
13
21
  }
14
- isDuplicate(event, href) {
15
- const activation = {
16
- button: event.button,
17
- href,
18
- clientX: event.clientX,
19
- clientY: event.clientY,
20
- at: Date.now()
21
- };
22
- const previous = this.lastActivation;
23
- this.lastActivation = activation;
24
- return !!(previous && previous.href === activation.href && previous.button === activation.button && previous.clientX === activation.clientX && previous.clientY === activation.clientY && activation.at - previous.at <= DEDUPE_MS);
22
+ stop(discard = false) {
23
+ if (this.stopped) return;
24
+ this.stopped = true;
25
+ for (const cleanup of this.cleanups.reverse()) try {
26
+ cleanup({ discard });
27
+ } catch (error) {
28
+ this.runtime.log("cleanup failed", error);
29
+ }
30
+ this.cleanups.length = 0;
31
+ this.handlers.clear();
32
+ }
33
+ async setup(extension) {
34
+ if (this.stopped) return;
35
+ const subscriptions = [];
36
+ try {
37
+ const cleanup = await extension.setup({
38
+ ...this.runtime,
39
+ on: (hook, handler) => {
40
+ if (this.stopped) return () => {};
41
+ let handlers = this.handlers.get(hook);
42
+ if (!handlers) {
43
+ handlers = /* @__PURE__ */ new Set();
44
+ this.handlers.set(hook, handlers);
45
+ }
46
+ handlers.add(handler);
47
+ const off = () => handlers.delete(handler);
48
+ subscriptions.push(off);
49
+ return off;
50
+ }
51
+ });
52
+ if (this.stopped) cleanup?.({ discard: true });
53
+ else if (cleanup) this.cleanups.push(cleanup);
54
+ } catch (error) {
55
+ for (const off of subscriptions) off();
56
+ this.runtime.log(`${extension.name} setup failed`, error);
57
+ }
25
58
  }
26
59
  };
27
- function isEligibleActivation(event) {
28
- if (event.defaultPrevented) return false;
29
- if (event.type === "click") return event.button === 0;
30
- if (event.type === "auxclick") return event.button === 1;
31
- return false;
32
- }
33
- function findOutboundAnchor(event) {
34
- for (const node of event.composedPath()) if (isOutboundAnchor(node)) return node;
35
- let node = event.target;
36
- while (node) {
37
- if (isOutboundAnchor(node)) return node;
38
- node = node.parentNode;
39
- }
40
- return null;
41
- }
42
- function isOutboundAnchor(node) {
43
- return node instanceof HTMLAnchorElement && (node.protocol === "http:" || node.protocol === "https:") && node.host !== location.host;
44
- }
45
60
  //#endregion
46
61
  //#region src/analytics.ts
47
- let instance = null;
48
- let pendingConsentMode;
49
- function getInstance() {
50
- return instance;
51
- }
52
- function activeInstance() {
53
- if (typeof window === "undefined" || isTrackingDisabled()) return null;
54
- return instance;
55
- }
56
- function trackEvent(eventName, properties) {
57
- activeInstance()?.track(eventName, properties);
58
- }
59
- async function identify(externalId, email, options) {
60
- return activeInstance()?.identify(externalId, email, options) ?? false;
61
- }
62
- function logout(resetAnonymousIdentity = true) {
63
- activeInstance()?.logout(resetAnonymousIdentity);
64
- }
65
- function setConsentMode(mode) {
66
- if (instance) instance.setConsentMode(mode);
67
- else pendingConsentMode = mode;
68
- }
69
- function optIn() {
70
- setConsentMode("granted");
71
- }
72
- function optOut() {
73
- setConsentMode("denied");
74
- }
75
- function reportError(error) {
76
- activeInstance()?.reportError(error);
62
+ const navigationListeners = /* @__PURE__ */ new Set();
63
+ const historyPatches = {};
64
+ function patchHistory(method) {
65
+ if (historyPatches[method]?.owner === history) return;
66
+ const original = history[method];
67
+ const wrapped = function(data, unused, url) {
68
+ Reflect.apply(original, this, [
69
+ data,
70
+ unused,
71
+ url
72
+ ]);
73
+ for (const listener of navigationListeners) listener();
74
+ };
75
+ history[method] = wrapped;
76
+ historyPatches[method] = {
77
+ owner: history,
78
+ original,
79
+ wrapped
80
+ };
77
81
  }
78
- function isTrackingDisabled() {
79
- const value = getStorageItem("localStorage", "disable-faststats");
80
- return value === "true" || value === "1";
82
+ function observeHistory(listener) {
83
+ navigationListeners.add(listener);
84
+ patchHistory("pushState");
85
+ patchHistory("replaceState");
86
+ return () => {
87
+ navigationListeners.delete(listener);
88
+ if (navigationListeners.size) return;
89
+ for (const method of ["pushState", "replaceState"]) {
90
+ const patch = historyPatches[method];
91
+ if (patch && history[method] === patch.wrapped) {
92
+ history[method] = patch.original;
93
+ delete historyPatches[method];
94
+ }
95
+ }
96
+ };
81
97
  }
82
98
  var WebAnalytics = class {
83
99
  options;
84
100
  baseUrl;
85
101
  debug;
102
+ cleanup = [];
86
103
  started = false;
87
- destroyed = false;
88
- epoch = 0;
89
104
  heartbeatTimer = null;
105
+ extensions = null;
90
106
  pageKey = "";
91
107
  entry = {
92
108
  path: "",
@@ -98,365 +114,327 @@ var WebAnalytics = class {
98
114
  visitStartedAt = null;
99
115
  pendingPageviewTrigger = null;
100
116
  consentMode;
101
- pendingBehavior;
102
- cleanup = [];
103
- childTrackers = [];
104
- outboundLinks = new OutboundLinkTracker();
105
117
  constructor(options) {
106
118
  this.options = options;
119
+ if (!options.siteKey.trim()) throw new Error("siteKey is required");
107
120
  this.baseUrl = resolveBaseUrl(options.baseUrl, ANALYTICS_BASE);
108
121
  this.debug = options.debug ?? false;
109
- this.consentMode = pendingConsentMode ?? options.consent?.mode ?? "granted";
110
- pendingConsentMode = void 0;
111
- this.pendingBehavior = options.consent?.pendingBehavior ?? "anonymous";
112
- if (options.autoTrack ?? true) {
113
- if (typeof window === "undefined") return;
114
- if (isTrackingDisabled()) {
115
- this.log("disabled");
116
- return;
117
- }
118
- setTimeout(() => void this.start(), 0);
119
- }
120
- }
121
- log(msg) {
122
- if (this.debug) console.log(`[Analytics] ${msg}`);
123
- }
124
- isTrackingBlocked() {
125
- return typeof window === "undefined" || this.destroyed || isTrackingDisabled();
126
- }
127
- ensureActive() {
128
- if (this.isTrackingBlocked()) return false;
129
- if (!this.started) this.start();
130
- return this.started && !this.destroyed && instance === this;
131
- }
132
- canTrack() {
133
- return this.consentMode === "granted" || this.consentMode === "pending" && this.pendingBehavior === "anonymous";
134
- }
135
- canSendEvents() {
136
- return !this.isTrackingBlocked() && this.canTrack();
137
- }
138
- cookieless() {
139
- return !!this.options.cookieless || this.consentMode === "pending" && this.pendingBehavior === "anonymous";
140
- }
141
- touch() {
142
- if (this.canTrack()) touchActivity(this.options.siteKey, this.cookieless());
143
- }
144
- bind(target, type, listener, options) {
145
- target.addEventListener(type, listener, options);
146
- this.cleanup.push(() => target.removeEventListener(type, listener, options));
147
- }
148
- stopHeartbeat() {
149
- if (!this.heartbeatTimer) return;
150
- clearInterval(this.heartbeatTimer);
151
- this.heartbeatTimer = null;
152
- }
153
- loadChild(name, create) {
154
- const epoch = this.epoch;
155
- create().then(async (tracker) => {
156
- if (this.epoch !== epoch || !this.started) return tracker.stop?.(true);
157
- await tracker.start();
158
- if (this.epoch !== epoch || !this.started) return tracker.stop?.(true);
159
- this.childTrackers.push(tracker);
160
- this.log(`${name} loaded`);
161
- }).catch((error) => this.log(`failed to initialize ${name} tracker: ${String(error)}`));
162
- }
163
- loadOptionalTrackers() {
164
- const o = this.options;
165
- const base = {
166
- siteKey: o.siteKey,
167
- baseUrl: this.baseUrl,
168
- debug: this.debug,
169
- trackHash: o.trackHash ?? false
170
- };
171
- if (o.sessionReplays?.enabled) this.loadChild("replay", async () => {
172
- const { default: ReplayTracker } = await import("./replay.js");
173
- return new ReplayTracker({
174
- ...base,
175
- ...o.replayOptions
176
- });
177
- });
178
- if (o.errorTracking?.enabled) this.loadChild("error", async () => {
179
- const { default: ErrorTracker } = await import("./error.js");
180
- return new ErrorTracker({
181
- ...base,
182
- sdkName: o.sdkName,
183
- sdkVersion: o.sdkVersion
184
- });
185
- });
186
- if (o.webVitals?.enabled) this.loadChild("web-vitals", async () => {
187
- const { default: WebVitalsTracker } = await import("./web-vitals.js");
188
- return new WebVitalsTracker({
189
- ...base,
190
- attribution: o.webVitals?.attribution ?? false
191
- });
192
- });
193
- }
194
- enterPage() {
195
- const includeHash = this.options.trackHash ?? false;
196
- const hash = includeHash ? location.hash : "";
197
- this.entry = {
198
- path: `${location.pathname}${hash}`,
199
- url: sanitizeUrl(location.href, includeHash),
200
- search: location.search
201
- };
202
- this.left = false;
203
- this.startVisit();
204
- }
205
- beginTracking(trigger) {
206
- this.touch();
207
- this.loadOptionalTrackers();
208
- this.enterPage();
209
- this.pageKey = "";
210
- if (document.visibilityState === "visible") {
211
- this.pageview({ trigger });
212
- return;
213
- }
214
- this.pendingPageviewTrigger = trigger;
122
+ this.consentMode = options.consent ?? "granted";
215
123
  }
216
- onLinkClick = (event) => {
217
- const href = this.outboundLinks.getHref(event);
218
- if (href) this.send("outbound_link", { outbound_link: sanitizeUrl(href) });
219
- };
220
- onVisibilityChange = () => {
221
- this.touch();
222
- if (document.visibilityState === "hidden") {
223
- this.pauseVisit();
224
- for (const tracker of this.childTrackers) tracker.onPageHidden?.(false, false);
225
- this.stopHeartbeat();
226
- return;
227
- }
228
- this.startHeartbeat();
229
- this.resumeVisit();
230
- const trigger = this.pendingPageviewTrigger;
231
- this.pendingPageviewTrigger = null;
232
- if (trigger) this.pageview({ trigger });
233
- };
234
- onPageHide = (event) => {
235
- const persisted = event.persisted;
236
- this.pauseVisit();
237
- for (const tracker of this.childTrackers) tracker.onPageHidden?.(persisted, !persisted);
238
- if (!persisted) this.leavePage();
239
- };
240
- onPageShow = (event) => {
241
- if (document.visibilityState === "visible") this.resumeVisit();
242
- for (const tracker of this.childTrackers) tracker.onPageShow?.(event.persisted);
243
- };
244
- async start() {
245
- if (this.started) return;
246
- if (this.isTrackingBlocked()) {
247
- if (isTrackingDisabled()) this.log("disabled");
248
- return;
249
- }
250
- if (instance && instance !== this) {
251
- this.log("already started by another instance");
252
- return;
253
- }
124
+ start() {
125
+ if (this.started !== false || typeof window === "undefined") return;
254
126
  this.started = true;
255
- instance = this;
256
- setCookielessMode(this.cookieless());
257
- this.bind(document, "click", this.onLinkClick, true);
258
- this.bind(document, "auxclick", this.onLinkClick, true);
259
- this.bind(document, "visibilitychange", this.onVisibilityChange);
260
- this.bind(window, "pagehide", this.onPageHide);
261
- this.bind(window, "pageshow", this.onPageShow);
262
- this.bind(window, "popstate", () => this.navigate());
263
- if (this.options.trackHash ?? false) this.bind(window, "hashchange", () => this.navigate());
264
- for (const method of ["pushState", "replaceState"]) {
265
- const original = history[method];
266
- const wrapped = (data, unused, url) => {
267
- Reflect.apply(original, history, [
268
- data,
269
- unused,
270
- url
271
- ]);
272
- this.navigate();
273
- };
274
- history[method] = wrapped;
275
- this.cleanup.push(() => {
276
- if (history[method] === wrapped) history[method] = original;
277
- });
278
- }
279
- this.startHeartbeat();
280
- if (this.canTrack()) this.beginTracking("load");
127
+ this.installBrowserLifecycle();
128
+ if (this.canTrack()) this.activate("load");
281
129
  }
282
130
  destroy() {
283
- if (this.destroyed) return;
284
- this.epoch++;
285
- if (this.started && typeof window !== "undefined") this.leavePage();
131
+ if (this.started === null) return;
132
+ if (this.started && typeof window !== "undefined" && this.canTrack()) this.leavePage();
133
+ this.started = null;
286
134
  this.pendingPageviewTrigger = null;
287
135
  this.stopHeartbeat();
288
- for (const fn of this.cleanup.splice(0)) fn();
289
- for (const tracker of this.childTrackers.splice(0)) tracker.stop?.();
290
- if (instance === this) instance = null;
291
- this.started = false;
292
- this.destroyed = true;
136
+ for (const cleanup of this.cleanup.splice(0).reverse()) cleanup();
137
+ this.stopExtensions(false);
293
138
  }
294
- pageview(extra = {}) {
139
+ pageview(properties = {}) {
295
140
  if (!this.ensureActive()) return;
296
- const trackHash = this.options.trackHash ?? false;
297
- const key = `${location.pathname}|${location.search}|${trackHash ? location.hash : ""}`;
141
+ const includeHash = this.options.trackHash ?? false;
142
+ const key = `${location.pathname}|${location.search}|${includeHash ? location.hash : ""}`;
298
143
  if (key === this.pageKey) return;
299
144
  this.pageKey = key;
300
- this.send("pageview", extra);
145
+ this.send("pageview", properties);
301
146
  }
302
- track(name, extra = {}) {
147
+ track(name, properties = {}) {
303
148
  if (!this.ensureActive()) return;
304
149
  const eventName = name.trim();
305
150
  if (!eventName || eventName === "pageview" || eventName === "page_leave" || eventName === "error" || eventName === "outbound_link") return;
306
- this.send(eventName, extra);
307
- }
308
- async identify(externalId, email, options = {}) {
309
- if (!this.ensureActive() || !this.canTrack() || this.cookieless()) return false;
310
- const trimmedExternalId = externalId.trim();
311
- const trimmedEmail = email.trim();
312
- if (!trimmedExternalId || !trimmedEmail) return false;
313
- return sendData({
151
+ this.send(eventName, properties);
152
+ }
153
+ async identify(userOrExternalId, email, options = {}) {
154
+ if (!this.ensureActive() || !this.canTrack() || this.isCookieless()) return false;
155
+ const user = typeof userOrExternalId === "string" ? {
156
+ id: userOrExternalId,
157
+ email,
158
+ ...options
159
+ } : userOrExternalId;
160
+ const externalId = user.id.trim();
161
+ if (!externalId) return false;
162
+ const clearFields = [
163
+ "email",
164
+ "name",
165
+ "phone",
166
+ "avatarUrl"
167
+ ].filter((field) => user[field] === null);
168
+ return await sendData({
314
169
  url: `${this.baseUrl}${URLS.identify}`,
315
170
  data: JSON.stringify({
316
171
  token: this.options.siteKey,
317
172
  identifier: this.getAnonymousId(),
318
- externalId: trimmedExternalId,
319
- email: trimmedEmail,
320
- name: options.name?.trim() || void 0,
321
- phone: options.phone?.trim() || void 0,
322
- avatarUrl: options.avatarUrl?.trim() || void 0,
323
- traits: options.traits ?? {}
173
+ externalId,
174
+ email: user.email?.trim() || void 0,
175
+ name: user.name?.trim() || void 0,
176
+ phone: user.phone?.trim() || void 0,
177
+ avatarUrl: user.avatarUrl?.trim() || void 0,
178
+ traits: user.traits,
179
+ replaceTraits: user.traitMode === "replace",
180
+ unsetTraits: user.unsetTraits,
181
+ aliases: user.aliases,
182
+ clearFields
324
183
  }),
325
184
  contentType: "text/plain",
326
185
  debug: this.debug,
327
- debugPrefix: "[Analytics] identify",
186
+ debugPrefix: "[FastStats] identify",
328
187
  useBeacon: false
329
188
  });
330
189
  }
331
190
  logout(resetAnonymousIdentity = true) {
332
191
  if (!this.ensureActive()) return;
333
- if (resetAnonymousIdentity) resetAnonymousId(this.options.siteKey, this.cookieless());
334
- resetSession(this.options.siteKey);
192
+ if (resetAnonymousIdentity) resetAnonymousId(this.options.siteKey, this.isCookieless());
193
+ resetSession(this.options.siteKey, this.isCookieless());
335
194
  }
336
195
  setConsentMode(mode) {
337
196
  if (mode === this.consentMode) return;
338
197
  const wasTracking = this.canTrack();
198
+ const wasCookieless = this.isCookieless();
199
+ const willTrack = mode !== "denied";
200
+ const willBeCookieless = this.options.cookieless === true || mode === "anonymous";
201
+ if (wasTracking && willTrack && wasCookieless !== willBeCookieless) this.leavePage();
339
202
  this.consentMode = mode;
340
- const tracking = this.canTrack();
341
- const cookieless = this.cookieless();
342
- setCookielessMode(cookieless);
343
- if (!tracking) {
344
- this.epoch++;
203
+ if (!willTrack) {
345
204
  this.pauseVisit();
346
205
  this.stopHeartbeat();
347
206
  this.pendingPageviewTrigger = null;
348
- for (const tracker of this.childTrackers.splice(0)) tracker.stop?.(true);
207
+ this.stopExtensions(true);
349
208
  return;
350
209
  }
351
- for (const tracker of this.childTrackers) tracker.setCookielessMode?.(cookieless);
352
- if (!wasTracking && this.started) {
353
- this.startHeartbeat();
354
- this.beginTracking("consent");
355
- }
356
- }
357
- getConsentMode() {
358
- return this.consentMode;
210
+ if (wasTracking) {
211
+ this.extensions?.emit("consentChange", { cookieless: willBeCookieless });
212
+ if (wasCookieless !== willBeCookieless) {
213
+ this.enterPage();
214
+ this.pageKey = "";
215
+ this.pageview({ trigger: "consent" });
216
+ }
217
+ } else if (this.started) this.activate("consent");
359
218
  }
360
219
  getAnonymousId() {
361
- if (!this.canTrack()) return "";
362
- return getAnonymousId(this.options.siteKey, this.cookieless());
220
+ return this.canTrack() ? getAnonymousId(this.options.siteKey, this.isCookieless()) : "";
363
221
  }
364
- getSessionId() {
365
- if (!this.canTrack()) return "";
366
- return getSessionContext(this.options.siteKey, this.cookieless()).sessionId;
222
+ reportError(error) {
223
+ if (!this.ensureActive() || !this.canTrack()) return;
224
+ this.extensions?.emit("error", { error });
367
225
  }
368
- getWindowId() {
369
- if (!this.canTrack()) return "";
370
- return getSessionContext(this.options.siteKey, this.cookieless()).windowId;
226
+ canTrack() {
227
+ return this.consentMode !== "denied";
371
228
  }
372
- async checkFeatureFlag(key, attributes, opts) {
373
- if (typeof window === "undefined" || isTrackingDisabled() || !this.canTrack()) return { value: "false" };
374
- const externalId = opts?.externalId?.trim();
375
- const identifier = this.getAnonymousId();
376
- if (!externalId && !identifier) return { value: "false" };
377
- const { fetchFeatureFlagEvaluation } = await import("./feature-flags.js");
378
- return fetchFeatureFlagEvaluation(key, {
379
- baseUrl: resolveBaseUrl(this.options.featureFlagsBaseUrl, FEATURE_FLAGS_BASE),
380
- projectToken: this.options.siteKey,
381
- ...identifier ? { identifier } : {},
382
- ...externalId ? { externalId } : {},
383
- sessionId: this.getSessionId(),
384
- attributes,
385
- signal: opts?.signal
386
- });
229
+ isCookieless() {
230
+ return this.options.cookieless === true || this.consentMode === "anonymous";
387
231
  }
388
- reportError(error) {
389
- if (!this.ensureActive() || !this.options.errorTracking?.enabled || !this.canTrack()) return;
390
- this.childTrackers.find((tracker) => tracker.captureError)?.captureError?.(error);
232
+ ensureActive() {
233
+ return this.started === true && typeof window !== "undefined";
391
234
  }
392
- send(event, properties = {}, dims = {}) {
393
- if (!this.canSendEvents()) return;
394
- const cookieless = this.cookieless();
395
- const id = getAnonymousId(this.options.siteKey, cookieless);
396
- const ctx = getSessionContext(this.options.siteKey, cookieless);
397
- this.log(event);
398
- sendData({
399
- url: `${this.baseUrl}${URLS.events}`,
400
- data: JSON.stringify({
401
- token: this.options.siteKey,
402
- ...id ? { userId: id } : {},
403
- sessionId: ctx.sessionId,
404
- windowId: ctx.windowId,
405
- event,
406
- ...getPageContext(this.options.trackHash ?? false),
407
- ...dims,
408
- properties
409
- }),
410
- contentType: "text/plain",
235
+ activate(trigger) {
236
+ this.touch();
237
+ this.startExtensions();
238
+ this.enterPage();
239
+ this.pageKey = "";
240
+ this.startHeartbeat();
241
+ if (document.visibilityState === "visible") this.pageview({ trigger });
242
+ else this.pendingPageviewTrigger = trigger;
243
+ }
244
+ startExtensions() {
245
+ if (this.extensions || !this.options.extensions?.length) return;
246
+ const host = new ExtensionHost(this.options.extensions, {
247
+ siteKey: this.options.siteKey,
248
+ baseUrl: this.baseUrl,
411
249
  debug: this.debug,
412
- debugPrefix: `[Analytics] ${event}`
250
+ trackHash: this.options.trackHash ?? false,
251
+ sdkName: this.options.sdkName,
252
+ sdkVersion: this.options.sdkVersion,
253
+ isCookieless: () => this.isCookieless(),
254
+ getAnonymousId: () => this.getAnonymousId(),
255
+ getSession: () => getSessionContext(this.options.siteKey, this.isCookieless()),
256
+ capture: (event, properties) => this.send(event, properties),
257
+ log: (...details) => {
258
+ if (this.debug) console.log(...details);
259
+ }
413
260
  });
261
+ this.extensions = host;
262
+ }
263
+ stopExtensions(discard) {
264
+ this.extensions?.stop(discard);
265
+ this.extensions = null;
266
+ }
267
+ installBrowserLifecycle() {
268
+ this.bind(document, "visibilitychange", this.onVisibilityChange);
269
+ this.bind(window, "pagehide", this.onPageHide);
270
+ this.bind(window, "pageshow", this.onPageShow);
271
+ this.bind(window, "popstate", this.onNavigate);
272
+ if (this.options.trackHash) this.bind(window, "hashchange", this.onNavigate);
273
+ this.cleanup.push(observeHistory(() => this.navigate()));
274
+ }
275
+ bind(target, type, listener, options) {
276
+ target.addEventListener(type, listener, options);
277
+ this.cleanup.push(() => target.removeEventListener(type, listener, options));
278
+ }
279
+ onNavigate = () => this.navigate();
280
+ onVisibilityChange = () => {
281
+ if (!this.canTrack()) return;
282
+ this.touch();
283
+ if (document.visibilityState === "hidden") {
284
+ this.pauseVisit();
285
+ this.extensions?.emit("pageHide", {
286
+ persisted: false,
287
+ terminal: false
288
+ });
289
+ this.stopHeartbeat();
290
+ return;
291
+ }
292
+ this.startHeartbeat();
293
+ this.resumeVisit();
294
+ const trigger = this.pendingPageviewTrigger;
295
+ this.pendingPageviewTrigger = null;
296
+ if (trigger) this.pageview({ trigger });
297
+ };
298
+ onPageHide = (event) => {
299
+ if (!this.canTrack()) return;
300
+ const persisted = event.persisted;
301
+ this.pauseVisit();
302
+ this.extensions?.emit("pageHide", {
303
+ persisted,
304
+ terminal: !persisted
305
+ });
306
+ if (!persisted) this.leavePage();
307
+ };
308
+ onPageShow = (event) => {
309
+ if (!this.canTrack()) return;
310
+ if (document.visibilityState === "visible") this.resumeVisit();
311
+ this.extensions?.emit("pageShow", { persisted: event.persisted });
312
+ };
313
+ navigate() {
314
+ if (this.started !== true || !this.canTrack()) return;
315
+ const includeHash = this.options.trackHash ?? false;
316
+ if (`${location.pathname}${includeHash ? location.hash : ""}` === this.entry.path && location.search === this.entry.search) return;
317
+ const url = sanitizeUrl(location.href, includeHash);
318
+ this.extensions?.emit("pageChange", { url });
319
+ this.leavePage();
320
+ this.enterPage();
321
+ this.pageview({ trigger: "navigation" });
322
+ }
323
+ enterPage() {
324
+ const includeHash = this.options.trackHash ?? false;
325
+ const hash = includeHash ? location.hash : "";
326
+ this.entry = {
327
+ path: `${location.pathname}${hash}`,
328
+ url: sanitizeUrl(location.href, includeHash),
329
+ search: location.search
330
+ };
331
+ this.left = false;
332
+ this.visitDuration = 0;
333
+ this.visitStartedAt = document.visibilityState === "visible" ? Date.now() : null;
414
334
  }
415
335
  leavePage() {
416
- if (this.destroyed || this.left) return;
336
+ if (this.started !== true || this.left || !this.canTrack()) return;
417
337
  this.pauseVisit();
418
338
  this.left = true;
419
- const ctx = getSessionContext(this.options.siteKey, this.cookieless());
339
+ const session = getSessionContext(this.options.siteKey, this.isCookieless());
420
340
  this.send("page_leave", {
421
341
  time_on_page: this.visitDuration,
422
- session_duration: Date.now() - ctx.sessionStart
342
+ session_duration: Date.now() - session.sessionStart
423
343
  }, {
424
344
  page: this.entry.path,
425
345
  url: this.entry.url
426
346
  });
427
347
  }
428
- startVisit() {
429
- this.visitDuration = 0;
430
- this.visitStartedAt = document.visibilityState === "visible" ? Date.now() : null;
431
- }
432
348
  pauseVisit() {
433
349
  if (this.visitStartedAt === null) return;
434
350
  this.visitDuration += Math.max(0, Date.now() - this.visitStartedAt);
435
351
  this.visitStartedAt = null;
436
352
  }
437
353
  resumeVisit() {
438
- if (this.left || this.visitStartedAt !== null) return;
439
- this.visitStartedAt = Date.now();
354
+ if (!this.left && this.visitStartedAt === null) this.visitStartedAt = Date.now();
355
+ }
356
+ touch() {
357
+ if (this.canTrack()) touchActivity(this.options.siteKey, this.isCookieless());
440
358
  }
441
359
  startHeartbeat() {
442
360
  this.stopHeartbeat();
443
361
  this.heartbeatTimer = setInterval(() => {
444
- if (document.visibilityState === "hidden") {
445
- this.stopHeartbeat();
446
- return;
447
- }
362
+ if (document.visibilityState === "hidden") return this.stopHeartbeat();
448
363
  this.touch();
449
- }, 300 * 1e3);
364
+ }, 3e5);
450
365
  }
451
- navigate() {
452
- if (!this.started || this.destroyed) return;
453
- const trackHash = this.options.trackHash ?? false;
454
- if (`${location.pathname}${trackHash ? location.hash : ""}` === this.entry.path && location.search === this.entry.search) return;
455
- for (const tracker of this.childTrackers) tracker.trackPageChange?.(sanitizeUrl(location.href, trackHash));
456
- this.leavePage();
457
- this.enterPage();
458
- this.pageview({ trigger: "navigation" });
366
+ stopHeartbeat() {
367
+ if (!this.heartbeatTimer) return;
368
+ clearInterval(this.heartbeatTimer);
369
+ this.heartbeatTimer = null;
370
+ }
371
+ send(event, properties = {}, dimensions = {}) {
372
+ if (this.started !== true || !this.canTrack()) return;
373
+ const cookieless = this.isCookieless();
374
+ const userId = getAnonymousId(this.options.siteKey, cookieless);
375
+ const session = getSessionContext(this.options.siteKey, cookieless);
376
+ let data;
377
+ try {
378
+ data = JSON.stringify({
379
+ token: this.options.siteKey,
380
+ ...userId ? { userId } : {},
381
+ sessionId: session.sessionId,
382
+ windowId: session.windowId,
383
+ event,
384
+ ...getPageContext(this.options.trackHash ?? false),
385
+ ...dimensions,
386
+ properties
387
+ });
388
+ } catch (error) {
389
+ if (this.debug) console.log("serialization failed", event, error);
390
+ return;
391
+ }
392
+ sendData({
393
+ url: `${this.baseUrl}${URLS.events}`,
394
+ data,
395
+ contentType: "text/plain",
396
+ debug: this.debug,
397
+ debugPrefix: "[FastStats]"
398
+ });
459
399
  }
460
400
  };
461
401
  //#endregion
462
- export { WebAnalytics, getInstance, identify, isTrackingDisabled, logout, optIn, optOut, reportError, setConsentMode, trackEvent };
402
+ //#region src/client.ts
403
+ let client;
404
+ /** Creates an independent client. Call `start()` when it should begin tracking. */
405
+ function createClient(options) {
406
+ return new WebAnalytics(options);
407
+ }
408
+ /** Initializes and starts the shared client used by the convenience functions. */
409
+ function init(options) {
410
+ if (client) return client;
411
+ client = createClient(options);
412
+ client.start();
413
+ return client;
414
+ }
415
+ /** Stops and forgets the shared client. Independent clients are unaffected. */
416
+ function shutdown() {
417
+ client?.destroy();
418
+ client = void 0;
419
+ }
420
+ function pageview(properties) {
421
+ client?.pageview(properties);
422
+ }
423
+ function track(name, properties) {
424
+ client?.track(name, properties);
425
+ }
426
+ function identify(userOrExternalId, email, options) {
427
+ if (!client) return Promise.resolve(false);
428
+ return typeof userOrExternalId === "string" ? client.identify(userOrExternalId, email, options) : client.identify(userOrExternalId);
429
+ }
430
+ function logout(resetAnonymousIdentity) {
431
+ client?.logout(resetAnonymousIdentity);
432
+ }
433
+ function setConsentMode(mode) {
434
+ client?.setConsentMode(mode);
435
+ }
436
+ function reportError(error) {
437
+ client?.reportError(error);
438
+ }
439
+ //#endregion
440
+ export { WebAnalytics, createClient, identify, init, logout, pageview, reportError, setConsentMode, shutdown, track };