@faststats/web 0.0.1 → 0.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,13 @@
1
+ # @faststats/web
2
+
3
+ ## 0.0.3
4
+
5
+ ### Patch Changes
6
+
7
+ - bb3dcd1: ESM only for now
8
+
9
+ ## 0.0.2
10
+
11
+ ### Patch Changes
12
+
13
+ - f1f66de: feat: add track event function
package/dist/module.d.ts CHANGED
@@ -399,6 +399,7 @@ declare global {
399
399
  }
400
400
  //#endregion
401
401
  //#region src/analytics.d.ts
402
+ declare function trackEvent(eventName: string, properties?: Record<string, unknown>): void;
402
403
  type Dict = Record<string, unknown>;
403
404
  interface SamplingOptions {
404
405
  percentage?: number;
@@ -428,7 +429,6 @@ interface IdentifyOptions {
428
429
  interface WebAnalyticsOptions {
429
430
  siteKey: string;
430
431
  endpoint?: string;
431
- baseUrl?: string;
432
432
  debug?: boolean;
433
433
  autoTrack?: boolean;
434
434
  trackHash?: boolean;
@@ -454,6 +454,7 @@ declare global {
454
454
  __FA_optIn?: () => void;
455
455
  __FA_optOut?: () => void;
456
456
  __FA_isTrackingDisabled?: () => boolean;
457
+ __FA_trackEvent?: (eventName: string, properties?: Record<string, unknown>) => void;
457
458
  __FA_webAnalyticsInstance?: WebAnalytics;
458
459
  }
459
460
  }
@@ -461,7 +462,6 @@ declare class WebAnalytics {
461
462
  private readonly options;
462
463
  private readonly endpoint;
463
464
  private readonly debug;
464
- private readonly baseUrl;
465
465
  private started;
466
466
  private pageKey;
467
467
  private navTimer;
@@ -479,7 +479,6 @@ declare class WebAnalytics {
479
479
  private log;
480
480
  private init;
481
481
  start(): void;
482
- private load;
483
482
  pageview(extra?: Dict): void;
484
483
  track(name: string, extra?: Dict): void;
485
484
  identify(externalId: string, email: string, options?: IdentifyOptions): void;
@@ -499,4 +498,4 @@ declare class WebAnalytics {
499
498
  private links;
500
499
  }
501
500
  //#endregion
502
- export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions };
501
+ export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions, trackEvent };
package/dist/module.js CHANGED
@@ -1,403 +1,3 @@
1
- //#region src/utils/identifiers.ts
2
- const SESSION_TIMEOUT = 1800 * 1e3;
3
- function getAnonymousId(cookieless) {
4
- if (cookieless) return "";
5
- return getOrCreateAnonymousId();
6
- }
7
- function getOrCreateAnonymousId() {
8
- if (typeof localStorage === "undefined") return "";
9
- const existingId = localStorage.getItem("faststats_anon_id");
10
- if (existingId) return existingId;
11
- const newId = crypto.randomUUID();
12
- localStorage.setItem("faststats_anon_id", newId);
13
- return newId;
14
- }
15
- function resetAnonymousId(cookieless) {
16
- if (cookieless || typeof localStorage === "undefined") return "";
17
- localStorage.removeItem("faststats_anon_id");
18
- return getOrCreateAnonymousId();
19
- }
20
- function getOrCreateSessionId() {
21
- if (typeof sessionStorage === "undefined") return "";
22
- const existingId = sessionStorage.getItem("session_id");
23
- const sessionTimestamp = sessionStorage.getItem("session_timestamp");
24
- if (existingId && sessionTimestamp) {
25
- if (Date.now() - Number.parseInt(sessionTimestamp, 10) < SESSION_TIMEOUT) {
26
- sessionStorage.setItem("session_timestamp", Date.now().toString());
27
- return existingId;
28
- }
29
- sessionStorage.removeItem("session_id");
30
- sessionStorage.removeItem("session_timestamp");
31
- sessionStorage.removeItem("session_start");
32
- }
33
- const now = Date.now().toString();
34
- const newId = crypto.randomUUID();
35
- sessionStorage.setItem("session_id", newId);
36
- sessionStorage.setItem("session_timestamp", now);
37
- sessionStorage.setItem("session_start", now);
38
- return newId;
39
- }
40
- function resetSessionId() {
41
- if (typeof sessionStorage === "undefined") return "";
42
- sessionStorage.removeItem("session_id");
43
- sessionStorage.removeItem("session_timestamp");
44
- sessionStorage.removeItem("session_start");
45
- return getOrCreateSessionId();
46
- }
47
- function refreshSessionTimestamp() {
48
- if (typeof sessionStorage === "undefined") return;
49
- if (sessionStorage.getItem("session_id")) sessionStorage.setItem("session_timestamp", Date.now().toString());
50
- }
51
- function getSessionStart() {
52
- if (typeof sessionStorage === "undefined") return Date.now();
53
- const start = sessionStorage.getItem("session_start");
54
- if (start) return Number.parseInt(start, 10);
55
- const ts = sessionStorage.getItem("session_timestamp");
56
- return ts ? Number.parseInt(ts, 10) : Date.now();
57
- }
58
-
59
- //#endregion
60
- //#region src/utils/types.ts
61
- function normalizeSamplingPercentage(value) {
62
- if (typeof value !== "number" || !Number.isFinite(value)) return 100;
63
- return Math.max(0, Math.min(100, value));
64
- }
65
-
66
- //#endregion
67
- //#region src/analytics.ts
68
- function isTrackingDisabled() {
69
- if (typeof localStorage === "undefined") return false;
70
- const value = localStorage.getItem("disable-faststats");
71
- return value === "true" || value === "1";
72
- }
73
- async function sendData(options) {
74
- const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]" } = options;
75
- const blob = data instanceof Blob ? data : new Blob([data], { type: contentType });
76
- if (navigator.sendBeacon?.(url, blob)) {
77
- if (debug) console.log(`${debugPrefix} ✓ Sent via beacon`);
78
- return true;
79
- }
80
- try {
81
- const response = await fetch(url, {
82
- method: "POST",
83
- body: data,
84
- headers: {
85
- "Content-Type": contentType,
86
- ...headers
87
- },
88
- keepalive: true
89
- });
90
- const success = response.ok;
91
- if (debug) if (success) console.log(`${debugPrefix} ✓ Sent via fetch`);
92
- else console.warn(`${debugPrefix} ✗ Failed: ${response.status}`);
93
- return success;
94
- } catch {
95
- if (debug) console.warn(`${debugPrefix} ✗ Failed to send`);
96
- return false;
97
- }
98
- }
99
- function getLinkEl(el) {
100
- while (el && !(el.tagName === "A" && el.href)) el = el.parentNode;
101
- return el;
102
- }
103
- function getUTM() {
104
- const params = {};
105
- if (!location.search) return params;
106
- const sp = new URLSearchParams(location.search);
107
- for (const k of [
108
- "utm_source",
109
- "utm_medium",
110
- "utm_campaign",
111
- "utm_term",
112
- "utm_content"
113
- ]) {
114
- const v = sp.get(k);
115
- if (v) params[k] = v;
116
- }
117
- return params;
118
- }
119
- var WebAnalytics = class {
120
- endpoint;
121
- debug;
122
- baseUrl;
123
- started = false;
124
- pageKey = "";
125
- navTimer = null;
126
- heartbeatTimer = null;
127
- scrollDepth = 0;
128
- pageEntryTime = 0;
129
- pagePath = "";
130
- pageUrl = "";
131
- pageHash = "";
132
- hasLeftCurrentPage = false;
133
- scrollHandler = null;
134
- consentMode;
135
- cookielessWhilePending;
136
- constructor(options) {
137
- this.options = options;
138
- this.endpoint = options.endpoint ?? "https://metrics.faststats.dev/v1/web";
139
- this.debug = options.debug ?? false;
140
- this.consentMode = options.consent?.mode ?? "granted";
141
- this.cookielessWhilePending = options.consent?.cookielessWhilePending ?? true;
142
- const script = typeof document !== "undefined" ? document.currentScript : null;
143
- const scriptBase = script?.src ? script.src.substring(0, script.src.lastIndexOf("/")) : "";
144
- this.baseUrl = (options.baseUrl ?? scriptBase) || (typeof window !== "undefined" ? window.location.origin : "");
145
- if (options.autoTrack ?? true) this.init();
146
- }
147
- log(msg) {
148
- if (this.debug) console.log(`[Analytics] ${msg}`);
149
- }
150
- init() {
151
- if (typeof window === "undefined") return;
152
- if (isTrackingDisabled()) {
153
- this.log("disabled");
154
- return;
155
- }
156
- if ("requestIdleCallback" in window) window.requestIdleCallback(() => this.start());
157
- else setTimeout(() => this.start(), 1);
158
- }
159
- start() {
160
- if (this.started || typeof window === "undefined") return;
161
- if (window.__FA_webAnalyticsInstance && window.__FA_webAnalyticsInstance !== this) {
162
- this.log("already started by another instance");
163
- return;
164
- }
165
- if (isTrackingDisabled()) {
166
- this.log("disabled");
167
- return;
168
- }
169
- this.started = true;
170
- window.__FA_webAnalyticsInstance = this;
171
- window.__FA_getAnonymousId = () => getAnonymousId(this.isCookielessMode());
172
- window.__FA_getSessionId = getOrCreateSessionId;
173
- window.__FA_sendData = sendData;
174
- window.__FA_identify = (externalId, email, options) => this.identify(externalId, email, options);
175
- window.__FA_logout = (resetAnonymousIdentity) => this.logout(resetAnonymousIdentity);
176
- window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
177
- window.__FA_optIn = () => this.optIn();
178
- window.__FA_optOut = () => this.optOut();
179
- window.__FA_isTrackingDisabled = isTrackingDisabled;
180
- const opts = this.options;
181
- const hasWebVitalsSampling = opts.webVitals?.sampling?.percentage !== void 0;
182
- const hasReplaySampling = opts.replayOptions?.samplingPercentage !== void 0 || opts.sessionReplays?.sampling?.percentage !== void 0;
183
- if (opts.errorTracking?.enabled ?? opts.trackErrors) this.load("error", "__FA_ErrorTracker", {
184
- siteKey: opts.siteKey,
185
- endpoint: this.endpoint,
186
- debug: this.debug,
187
- getCommonData: () => ({
188
- url: location.href,
189
- page: location.pathname,
190
- referrer: document.referrer || null
191
- })
192
- });
193
- if (opts.webVitals?.enabled ?? opts.trackWebVitals ?? hasWebVitalsSampling) this.load("web-vitals", "__FA_WebVitalsTracker", {
194
- siteKey: opts.siteKey,
195
- endpoint: this.endpoint,
196
- debug: this.debug,
197
- samplingPercentage: normalizeSamplingPercentage(opts.webVitals?.sampling?.percentage)
198
- });
199
- if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
200
- const replayOpts = opts.replayOptions ?? {};
201
- this.load("replay", "__FA_ReplayTracker", {
202
- siteKey: opts.siteKey,
203
- endpoint: this.endpoint,
204
- debug: this.debug,
205
- ...replayOpts,
206
- samplingPercentage: normalizeSamplingPercentage(replayOpts.samplingPercentage ?? opts.sessionReplays?.sampling?.percentage)
207
- });
208
- }
209
- this.enterPage();
210
- this.pageview({ trigger: "load" });
211
- this.links();
212
- this.trackScroll();
213
- this.startHeartbeat();
214
- document.addEventListener("visibilitychange", () => {
215
- if (document.visibilityState === "hidden") this.leavePage();
216
- else this.startHeartbeat();
217
- });
218
- window.addEventListener("pagehide", () => this.leavePage());
219
- window.addEventListener("popstate", () => this.navigate());
220
- if (opts.trackHash) window.addEventListener("hashchange", () => this.navigate());
221
- this.patch();
222
- }
223
- load(script, key, trackerOpts) {
224
- const el = document.createElement("script");
225
- el.src = `${this.baseUrl}/${script}.js`;
226
- el.async = true;
227
- el.onload = () => {
228
- const Ctor = window[key];
229
- if (Ctor) {
230
- new Ctor(trackerOpts).start();
231
- this.log(`${script} loaded`);
232
- }
233
- };
234
- el.onerror = () => {
235
- if (this.debug) console.warn(`[Analytics] ${script} failed`);
236
- };
237
- document.head.appendChild(el);
238
- }
239
- pageview(extra = {}) {
240
- const key = `${location.pathname}|${this.options.trackHash ?? false ? location.hash : ""}`;
241
- if (key === this.pageKey) return;
242
- this.pageKey = key;
243
- this.send("pageview", extra);
244
- }
245
- track(name, extra = {}) {
246
- this.send(name, extra);
247
- }
248
- identify(externalId, email, options = {}) {
249
- if (isTrackingDisabled()) return;
250
- if (this.isCookielessMode()) return;
251
- const trimmedExternalId = externalId.trim();
252
- const trimmedEmail = email.trim();
253
- if (!trimmedExternalId || !trimmedEmail) return;
254
- sendData({
255
- url: this.endpoint.replace(/\/v1\/web$/, "/v1/identify"),
256
- data: JSON.stringify({
257
- token: this.options.siteKey,
258
- identifier: getAnonymousId(false),
259
- externalId: trimmedExternalId,
260
- email: trimmedEmail,
261
- name: options.name?.trim() || void 0,
262
- phone: options.phone?.trim() || void 0,
263
- avatarUrl: options.avatarUrl?.trim() || void 0,
264
- traits: options.traits ?? {}
265
- }),
266
- contentType: "text/plain",
267
- debug: this.debug,
268
- debugPrefix: "[Analytics] identify"
269
- });
270
- }
271
- logout(resetAnonymousIdentity = true) {
272
- if (isTrackingDisabled()) return;
273
- if (resetAnonymousIdentity) resetAnonymousId(this.isCookielessMode());
274
- resetSessionId();
275
- }
276
- setConsentMode(mode) {
277
- this.consentMode = mode;
278
- }
279
- optIn() {
280
- this.setConsentMode("granted");
281
- }
282
- optOut() {
283
- this.setConsentMode("denied");
284
- }
285
- getConsentMode() {
286
- return this.consentMode;
287
- }
288
- isCookielessMode() {
289
- if (this.options.cookieless) return true;
290
- if (this.consentMode === "denied") return true;
291
- if (this.consentMode === "pending") return this.cookielessWhilePending;
292
- return false;
293
- }
294
- send(event, extra = {}) {
295
- const identifier = getAnonymousId(this.isCookielessMode());
296
- const payload = JSON.stringify({
297
- token: this.options.siteKey,
298
- ...identifier ? { userId: identifier } : {},
299
- sessionId: getOrCreateSessionId(),
300
- data: {
301
- event,
302
- page: location.pathname,
303
- referrer: document.referrer || null,
304
- title: document.title || "",
305
- url: location.href,
306
- ...getUTM(),
307
- ...extra
308
- }
309
- });
310
- this.log(event);
311
- sendData({
312
- url: this.endpoint,
313
- data: payload,
314
- contentType: "text/plain",
315
- debug: this.debug,
316
- debugPrefix: `[Analytics] ${event}`
317
- });
318
- }
319
- enterPage() {
320
- this.pageEntryTime = Date.now();
321
- this.pagePath = location.pathname;
322
- this.pageUrl = location.href;
323
- this.pageHash = location.hash;
324
- this.scrollDepth = 0;
325
- this.hasLeftCurrentPage = false;
326
- }
327
- leavePage() {
328
- if (this.hasLeftCurrentPage) return;
329
- this.hasLeftCurrentPage = true;
330
- const now = Date.now();
331
- this.send("page_leave", {
332
- page: this.pagePath,
333
- url: this.pageUrl,
334
- time_on_page: now - this.pageEntryTime,
335
- scroll_depth: this.scrollDepth,
336
- session_duration: now - getSessionStart()
337
- });
338
- }
339
- trackScroll() {
340
- if (this.scrollHandler) window.removeEventListener("scroll", this.scrollHandler);
341
- const update = () => {
342
- const doc = document.documentElement;
343
- const body = document.body;
344
- const viewportH = window.innerHeight;
345
- const docH = Math.max(doc.scrollHeight, body.scrollHeight);
346
- if (docH <= viewportH) {
347
- this.scrollDepth = 100;
348
- return;
349
- }
350
- const depth = Math.min(100, Math.round(((window.scrollY || doc.scrollTop) + viewportH) / docH * 100));
351
- if (depth > this.scrollDepth) this.scrollDepth = depth;
352
- };
353
- this.scrollHandler = update;
354
- update();
355
- window.addEventListener("scroll", update, { passive: true });
356
- }
357
- startHeartbeat() {
358
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
359
- this.heartbeatTimer = setInterval(() => {
360
- if (document.visibilityState === "hidden") {
361
- clearInterval(this.heartbeatTimer);
362
- this.heartbeatTimer = null;
363
- return;
364
- }
365
- refreshSessionTimestamp();
366
- }, 300 * 1e3);
367
- }
368
- navigate() {
369
- if (this.navTimer) clearTimeout(this.navTimer);
370
- this.navTimer = setTimeout(() => {
371
- this.navTimer = null;
372
- const pathChanged = location.pathname !== this.pagePath;
373
- const hashChanged = (this.options.trackHash ?? false) && location.hash !== this.pageHash;
374
- if (!pathChanged && !hashChanged) return;
375
- this.leavePage();
376
- this.enterPage();
377
- this.trackScroll();
378
- this.pageview({ trigger: "navigation" });
379
- }, 300);
380
- }
381
- patch() {
382
- const fire = () => this.navigate();
383
- for (const method of ["pushState", "replaceState"]) {
384
- const orig = history[method];
385
- history[method] = function(...args) {
386
- const result = orig.apply(this, args);
387
- fire();
388
- return result;
389
- };
390
- }
391
- }
392
- links() {
393
- const handler = (event) => {
394
- const link = getLinkEl(event.target);
395
- if (link && link.host !== location.host) this.track("outbound_link", { outbound_link: link.href });
396
- };
397
- document.addEventListener("click", handler);
398
- document.addEventListener("auxclick", handler);
399
- }
400
- };
401
-
402
- //#endregion
403
- export { WebAnalytics };
1
+ import{getRecordConsolePlugin as e}from"@rrweb/rrweb-plugin-console-record";import{record as t}from"rrweb";import{onCLS as n,onFCP as r,onINP as i,onLCP as a,onTTFB as o}from"web-vitals/attribution";var s=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;getCommonData;handledErrors=new WeakSet;errorCounts=new Map;flushTimer=null;started=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.flushInterval=e.flushInterval??5e3,this.maxQueueSize=e.maxQueueSize??50,this.getCommonData=e.getCommonData??(()=>({}))}start(){if(!(this.started||typeof window>`u`)){if(window.__FA_isTrackingDisabled?.()){this.debug&&console.log(`[ErrorTracker] Tracking disabled via localStorage`);return}this.started=!0,window.addEventListener(`error`,this.handleErrorEvent),window.addEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer=setInterval(()=>this.flush(),this.flushInterval),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.flush()}),window.addEventListener(`pagehide`,()=>this.flush()),this.debug&&console.log(`[ErrorTracker] Started listening for errors`)}}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.handleErrorEvent),window.removeEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}handleErrorEvent=e=>{let t=e.error;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:e.message||(t?.message??`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t?.stack||void 0,type:`error`})};handleRejection=e=>{let t=e.reason;if(t instanceof Error){if(this.handledErrors.has(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t.message);return}this.handledErrors.add(t)}this.queueError({message:t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`})};isExtensionError(e){return!!(e.filename?.startsWith(`chrome-extension://`)||e.stack&&e.stack.split(`
2
+ `).find(e=>e.trim().startsWith(`at `))?.includes(`chrome-extension://`))}parseStack(e){if(e)return e.split(`
3
+ `).map(e=>e.trim()).filter(e=>e.length>0)}async generateErrorHash(e){let t=[e.type,e.message,e.filename??``,e.lineno??``].join(`:`),n=new TextEncoder().encode(t),r=await crypto.subtle.digest(`SHA-256`,n);return`err_${Array.from(new Uint8Array(r)).map(e=>e.toString(16).padStart(2,`0`)).join(``)}`}async queueError(e){if(this.isExtensionError(e))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=await this.generateErrorHash(e),n=this.errorCounts.get(t);if(n)n.count++,this.debug&&console.log(`[ErrorTracker] Incremented count for ${t} to ${n.count}`);else{let n={error:e.type===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:this.parseStack(e.stack)};this.errorCounts.set(t,{entry:n,hash:t,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.handledErrors.has(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate manual capture:`,e.message);return}this.handledErrors.add(e),this.queueError({message:e.message,stack:e.stack,type:`error`})}flush(){if(this.errorCounts.size===0)return;let e=[];for(let{entry:t,hash:n,count:r}of this.errorCounts.values())e.push({...t,hash:n,count:r});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,e);let t=this.getCommonData(),n=window.__FA_getAnonymousId?.(),r=globalThis.__SOURCEMAPS_BUILD__,i=typeof r?.buildId==`string`&&r.buildId.trim().length>0?r.buildId:void 0,a={token:this.siteKey,...n?{userId:n}:{},sessionId:window.__FA_getSessionId?.(),...i?{buildId:i}:{},data:{url:t.url,page:t.page,referrer:t.referrer,title:typeof document<`u`?document.title:``},errors:e},o=JSON.stringify(a);this.debug&&console.log(`[ErrorTracker] Payload:`,o),window.__FA_sendData?.({url:this.endpoint,data:o,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};typeof window<`u`&&(window.__FA_ErrorTracker=s);function c(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}var l=class{endpoint;siteKey;debug;flushInterval;maxEvents;sampling;slimDOMOptions;maskAllInputs;maskInputOptions;blockClass;blockSelector;maskTextClass;maskTextSelector;checkoutEveryNms;checkoutEveryNth;samplingPercentage;recordConsole;events=[];flushTimer=null;stopRecording=void 0;started=!1;startTime=0;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs=3e3;constructor(e){this.siteKey=e.siteKey,this.endpoint=e.endpoint?.replace(/\/v1\/web$/,`/v1/replay`)??`https://metrics.faststats.dev/v1/replay`,this.debug=e.debug??!1,this.samplingPercentage=c(e.samplingPercentage),this.flushInterval=e.flushInterval??1e4,this.maxEvents=e.maxEvents??500,this.sampling=e.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=e.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=e.maskAllInputs??!0,this.maskInputOptions=e.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=e.blockClass,this.blockSelector=e.blockSelector,this.maskTextClass=e.maskTextClass,this.maskTextSelector=e.maskTextSelector,this.checkoutEveryNms=e.checkoutEveryNms??6e4,this.checkoutEveryNth=e.checkoutEveryNth,this.recordConsole=e.recordConsole??!0,this.sessionSamplingSeed=Math.random()*100,typeof window<`u`&&(this.compressionSupported=`CompressionStream`in window)}start(){if(this.started||typeof window>`u`)return;if(window.__FA_isTrackingDisabled?.()){this.debug&&console.log(`[Replay] Tracking disabled via localStorage`);return}if(this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;this.started=!0,this.startTime=Date.now(),this.debug&&console.log(`[Replay] Recording started`);let n={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(n.maskInputOptions=this.maskInputOptions),this.blockClass&&(n.blockClass=this.blockClass),this.blockSelector&&(n.blockSelector=this.blockSelector),this.maskTextClass&&(n.maskTextClass=this.maskTextClass),this.maskTextSelector&&(n.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(n.checkoutEveryNth=this.checkoutEveryNth),this.recordConsole&&(n.plugins=[e()]),this.stopRecording=t(n),this.flushTimer=setInterval(()=>{this.scheduleFlush()},this.flushInterval),window.addEventListener(`beforeunload`,this.handleUnload),window.addEventListener(`pagehide`,this.handleUnload),document.addEventListener(`visibilitychange`,this.handleVisibilityChange)}stop(){if(!this.started)return;this.started=!1,this.debug&&console.log(`[Replay] Recording stopped`),this.stopRecording?.(),this.stopRecording=void 0,this.flushTimer&&=(clearInterval(this.flushTimer),null),window.removeEventListener(`beforeunload`,this.handleUnload),window.removeEventListener(`pagehide`,this.handleUnload),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange);let e=Date.now()-this.startTime;if(e<this.minReplayLengthMs){this.events=[],this.debug&&console.log(`[Replay] Session too short (${e}ms), discarding events`);return}this.flush()}handleEvent(e,t){this.events.push(e),(t||this.events.length>=this.maxEvents)&&this.scheduleFlush()}scheduleFlush(){this.isFlushing||this.events.length===0||(typeof window<`u`&&`requestIdleCallback`in window?window.requestIdleCallback(()=>this.flush(),{timeout:2e3}):setTimeout(()=>this.flush(),0))}handleUnload=()=>{this.flush()};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush()):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};async flush(){if(this.events.length===0){this.processPendingBatches();return}let e=Date.now()-this.startTime;if(e<this.minReplayLengthMs){this.debug&&console.log(`[Replay] Too short (${e}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let t=this.events;this.events=[];let n=window.__FA_getAnonymousId?.(),r={token:this.siteKey,sessionId:window.__FA_getSessionId?.(),...n?{identifier:n}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:t};this.debug&&console.log(`[Replay] Sending ${t.length} events (seq: ${r.sequence})`);try{let e,t=!1;if(this.compressionSupported)try{e=await this.compress(JSON.stringify(r)),t=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),e=new Blob([JSON.stringify(r)],{type:`application/json`})}else e=new Blob([JSON.stringify(r)],{type:`application/json`});await this.send(e,t)||this.pendingBatches.push({batch:r,isCompressed:t,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.pendingBatches.push({batch:r,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(this.pendingBatches.length===0)return;let e=this.pendingBatches.shift();if(e){if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, dropping batch ${e.batch.sequence}`),this.processPendingBatches();return}e.retries++;try{let t;if(e.isCompressed&&this.compressionSupported)try{t=await this.compress(JSON.stringify(e.batch))}catch{t=new Blob([JSON.stringify(e.batch)],{type:`application/json`}),e.isCompressed=!1}else t=new Blob([JSON.stringify(e.batch)],{type:`application/json`});await this.send(t,e.isCompressed)?this.processPendingBatches():(this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries))}catch{this.debug&&console.warn(`[Replay] Retry ${e.retries} failed`),this.pendingBatches.push(e),setTimeout(()=>this.processPendingBatches(),1e3*e.retries)}}}async compress(e){if(!this.compressionSupported)throw Error(`Compression not supported`);let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();r.write(t),r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;if(this.debug){let e=(s.length/t.length*100).toFixed(1);console.log(`[Replay] Compressed: ${t.length} → ${s.length} bytes (${e}%)`)}return new Blob([s],{type:`application/octet-stream`})}async send(e,t){let n=t?`${this.endpoint}?encoding=gzip`:this.endpoint,r=(e.size/1024).toFixed(1);return window.__FA_sendData?.({url:n,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${r}KB`})??Promise.resolve(!1)}getSessionId(){return window.__FA_getSessionId?.()}};typeof window<`u`&&(window.__FA_ReplayTracker=l);function u(e){return e?``:d()}function d(){if(typeof localStorage>`u`)return``;let e=localStorage.getItem(`faststats_anon_id`);if(e)return e;let t=crypto.randomUUID();return localStorage.setItem(`faststats_anon_id`,t),t}function f(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),d())}function p(){if(typeof sessionStorage>`u`)return``;let e=sessionStorage.getItem(`session_id`),t=sessionStorage.getItem(`session_timestamp`);if(e&&t){if(Date.now()-Number.parseInt(t,10)<18e5)return sessionStorage.setItem(`session_timestamp`,Date.now().toString()),e;sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`)}let n=Date.now().toString(),r=crypto.randomUUID();return sessionStorage.setItem(`session_id`,r),sessionStorage.setItem(`session_timestamp`,n),sessionStorage.setItem(`session_start`,n),r}function m(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),p())}function h(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function g(){if(typeof sessionStorage>`u`)return Date.now();let e=sessionStorage.getItem(`session_start`);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(`session_timestamp`);return t?Number.parseInt(t,10):Date.now()}var _=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;constructor(e){this.siteKey=e.siteKey,this.endpoint=this.getVitalsEndpoint(e.endpoint??`https://metrics.faststats.dev/v1/web`),this.debug=e.debug??!1,this.samplingPercentage=c(e.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}getVitalsEndpoint(e){let t=new URL(e),n=t.pathname.split(`/`);return n[n.length-1]=`vitals`,t.pathname=n.join(`/`),t.toString()}start(){if(!(this.started||typeof window>`u`)){if(window.__FA_isTrackingDisabled?.()){this.debug&&console.log(`[WebVitals] Tracking disabled`);return}this.started=!0,document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()}),window.addEventListener(`pagehide`,()=>{this.finalizeAndFlush()}),this.debug&&console.log(`[WebVitals] Tracking started`),n(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),r(e=>this.captureMetric(e)),o(e=>this.captureMetric(e))}}captureMetric(e){if(this.flushed||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;let t=e.name,n={id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}},r=t===`FCP`||t===`TTFB`;this.metricsMap.set(t,{value:e.value,attributes:n,final:r}),this.debug&&console.log(`[WebVitals] ${t} captured: ${e.value}`+(r?` (final)`:``))}finalizeAndFlush(){if(!(this.flushed||this.metricsMap.size===0)){for(let[e,t]of this.metricsMap.entries())t.final||(t.final=!0,this.debug&&console.log(`[WebVitals] ${e} finalized: ${t.value}`));this.flushWithBeacon()}}buildPayload(){if(this.metricsMap.size===0)return null;let e=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:window.__FA_getSessionId?.(),vitals:e,metadata:{url:window.location.href}}),count:e.length}}flushWithBeacon(){let e=this.buildPayload();if(e){if(this.flushed=!0,this.debug){let t=Array.from(this.metricsMap.keys()).join(`, `);console.log(`[WebVitals] Sending final metrics (${e.count}): ${t}`)}fetch(this.endpoint,{method:`POST`,body:e.body,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.siteKey}`},keepalive:!0}).catch(()=>{this.debug&&console.warn(`[WebVitals] Failed to send metrics`)})}}};typeof window<`u`&&(window.__FA_WebVitalsTracker=_);function v(e,t){typeof window>`u`||y()||window.__FA_webAnalyticsInstance?.track(e,t??{})}function y(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function b(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`}=e,s=n instanceof Blob?n:new Blob([n],{type:r});if(navigator.sendBeacon?.(t,s))return a&&console.log(`${o} ✓ Sent via beacon`),!0;try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:!0}),s=e.ok;return a&&(s?console.log(`${o} ✓ Sent via fetch`):console.warn(`${o} ✗ Failed: ${e.status}`)),s}catch{return a&&console.warn(`${o} ✗ Failed to send`),!1}}function x(e){for(;e&&!(e.tagName===`A`&&e.href);)e=e.parentNode;return e}function S(){let e={};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`]){let r=t.get(n);r&&(e[n]=r)}return e}var C=class{endpoint;debug;started=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;constructor(e){this.options=e,this.endpoint=e.endpoint??`https://metrics.faststats.dev/v1/web`,this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(y()){this.log(`disabled`);return}`requestIdleCallback`in window?window.requestIdleCallback(()=>this.start()):setTimeout(()=>this.start(),1)}}start(){if(this.started||typeof window>`u`)return;if(window.__FA_webAnalyticsInstance&&window.__FA_webAnalyticsInstance!==this){this.log(`already started by another instance`);return}if(y()){this.log(`disabled`);return}this.started=!0,window.__FA_webAnalyticsInstance=this,window.__FA_getAnonymousId=()=>u(this.isCookielessMode()),window.__FA_getSessionId=p,window.__FA_sendData=b,window.__FA_identify=(e,t,n)=>this.identify(e,t,n),window.__FA_logout=e=>this.logout(e),window.__FA_setConsentMode=e=>this.setConsentMode(e),window.__FA_optIn=()=>this.optIn(),window.__FA_optOut=()=>this.optOut(),window.__FA_trackEvent=(e,t)=>this.track(e,t??{}),window.__FA_isTrackingDisabled=y;let e=this.options,t=e.webVitals?.sampling?.percentage!==void 0,n=e.replayOptions?.samplingPercentage!==void 0||e.sessionReplays?.sampling?.percentage!==void 0;if((e.errorTracking?.enabled??e.trackErrors)&&(new s({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,getCommonData:()=>({url:location.href,page:location.pathname,referrer:document.referrer||null})}).start(),this.log(`error loaded`)),(e.webVitals?.enabled??e.trackWebVitals??t)&&(new _({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,samplingPercentage:c(e.webVitals?.sampling?.percentage)}).start(),this.log(`web-vitals loaded`)),e.sessionReplays?.enabled??e.trackReplay??n){let t=e.replayOptions??{};new l({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,...t,samplingPercentage:c(t.samplingPercentage??e.sessionReplays?.sampling?.percentage)}).start(),this.log(`replay loaded`)}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),document.addEventListener(`visibilitychange`,()=>{document.visibilityState===`hidden`?this.leavePage():this.startHeartbeat()}),window.addEventListener(`pagehide`,()=>this.leavePage()),window.addEventListener(`popstate`,()=>this.navigate()),e.trackHash&&window.addEventListener(`hashchange`,()=>this.navigate()),this.patch()}pageview(e={}){let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.send(e,t)}identify(e,t,n={}){if(y()||this.isCookielessMode())return;let r=e.trim(),i=t.trim();!r||!i||b({url:this.endpoint.replace(/\/v1\/web$/,`/v1/identify`),data:JSON.stringify({token:this.options.siteKey,identifier:u(!1),externalId:r,email:i,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){y()||(e&&f(this.isCookielessMode()),m())}setConsentMode(e){this.consentMode=e}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){let n=u(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:p(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...S(),...t}});this.log(e),b({url:this.endpoint,data:r,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-g()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.heartbeatTimer&&clearInterval(this.heartbeatTimer),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){clearInterval(this.heartbeatTimer),this.heartbeatTimer=null;return}h()},300*1e3)}navigate(){this.navTimer&&clearTimeout(this.navTimer),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300)}patch(){let e=()=>this.navigate();for(let t of[`pushState`,`replaceState`]){let n=history[t];history[t]=function(...t){let r=n.apply(this,t);return e(),r}}}links(){let e=e=>{let t=x(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};document.addEventListener(`click`,e),document.addEventListener(`auxclick`,e)}};export{C as WebAnalytics,v as trackEvent};
package/package.json CHANGED
@@ -1,5 +1,6 @@
1
1
  {
2
2
  "name": "@faststats/web",
3
+ "repository": "https://github.com/faststats-dev/web-analytics",
3
4
  "main": "dist/module.js",
4
5
  "module": "dist/module.js",
5
6
  "types": "dist/module.d.ts",
@@ -14,11 +15,10 @@
14
15
  "publishConfig": {
15
16
  "access": "public"
16
17
  },
17
- "version": "0.0.1",
18
+ "version": "0.0.3",
18
19
  "scripts": {
19
- "build": "tsdown && cp -f dist/script.iife.js dist/script.js && cp -f dist/error.iife.js dist/error.js && cp -f dist/web-vitals.iife.js dist/web-vitals.js && cp -f dist/replay.iife.js dist/replay.js",
20
- "sync:example-assets": "mkdir -p ../../examples/spa-test/public && cp -f dist/script.iife.js ../../examples/spa-test/public/script.js && cp -f dist/error.iife.js ../../examples/spa-test/public/error.js && cp -f dist/web-vitals.iife.js ../../examples/spa-test/public/web-vitals.js && cp -f dist/replay.iife.js ../../examples/spa-test/public/replay.js",
21
- "dev": "bun run build && bun run sync:example-assets",
20
+ "build": "tsdown",
21
+ "dev": "bun run build",
22
22
  "deploy:worker": "wrangler deploy"
23
23
  },
24
24
  "devDependencies": {
package/src/analytics.ts CHANGED
@@ -1,4 +1,6 @@
1
+ import ErrorTracker from "./error";
1
2
  import type { ReplayTrackerOptions } from "./replay";
3
+ import ReplayTracker from "./replay";
2
4
  import {
3
5
  getAnonymousId,
4
6
  getOrCreateSessionId,
@@ -11,9 +13,18 @@ import {
11
13
  normalizeSamplingPercentage,
12
14
  type SendDataOptions,
13
15
  } from "./utils/types";
16
+ import WebVitalsTracker from "./web-vitals";
14
17
 
15
18
  export type { SendDataOptions };
16
19
 
20
+ export function trackEvent(
21
+ eventName: string,
22
+ properties?: Record<string, unknown>,
23
+ ): void {
24
+ if (typeof window === "undefined" || isTrackingDisabled()) return;
25
+ window.__FA_webAnalyticsInstance?.track(eventName, properties ?? {});
26
+ }
27
+
17
28
  export function isTrackingDisabled(): boolean {
18
29
  if (typeof localStorage === "undefined") return false;
19
30
  const value = localStorage.getItem("disable-faststats");
@@ -131,7 +142,6 @@ export interface IdentifyOptions {
131
142
  export interface WebAnalyticsOptions {
132
143
  siteKey: string;
133
144
  endpoint?: string;
134
- baseUrl?: string;
135
145
  debug?: boolean;
136
146
  autoTrack?: boolean;
137
147
  trackHash?: boolean;
@@ -162,6 +172,10 @@ declare global {
162
172
  __FA_optIn?: () => void;
163
173
  __FA_optOut?: () => void;
164
174
  __FA_isTrackingDisabled?: () => boolean;
175
+ __FA_trackEvent?: (
176
+ eventName: string,
177
+ properties?: Record<string, unknown>,
178
+ ) => void;
165
179
  __FA_webAnalyticsInstance?: WebAnalytics;
166
180
  }
167
181
  }
@@ -169,7 +183,6 @@ declare global {
169
183
  export class WebAnalytics {
170
184
  private readonly endpoint: string;
171
185
  private readonly debug: boolean;
172
- private readonly baseUrl: string;
173
186
  private started = false;
174
187
  private pageKey = "";
175
188
  private navTimer: ReturnType<typeof setTimeout> | null = null;
@@ -190,16 +203,6 @@ export class WebAnalytics {
190
203
  this.consentMode = options.consent?.mode ?? "granted";
191
204
  this.cookielessWhilePending =
192
205
  options.consent?.cookielessWhilePending ?? true;
193
- const script =
194
- typeof document !== "undefined"
195
- ? (document.currentScript as HTMLScriptElement | null)
196
- : null;
197
- const scriptBase = script?.src
198
- ? script.src.substring(0, script.src.lastIndexOf("/"))
199
- : "";
200
- this.baseUrl =
201
- (options.baseUrl ?? scriptBase) ||
202
- (typeof window !== "undefined" ? window.location.origin : "");
203
206
  if (options.autoTrack ?? true) this.init();
204
207
  }
205
208
 
@@ -244,6 +247,8 @@ export class WebAnalytics {
244
247
  window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
245
248
  window.__FA_optIn = () => this.optIn();
246
249
  window.__FA_optOut = () => this.optOut();
250
+ window.__FA_trackEvent = (eventName, props) =>
251
+ this.track(eventName, props ?? {});
247
252
  window.__FA_isTrackingDisabled = isTrackingDisabled;
248
253
 
249
254
  const opts = this.options;
@@ -254,7 +259,7 @@ export class WebAnalytics {
254
259
  opts.sessionReplays?.sampling?.percentage !== undefined;
255
260
 
256
261
  if (opts.errorTracking?.enabled ?? opts.trackErrors) {
257
- this.load("error", "__FA_ErrorTracker", {
262
+ new ErrorTracker({
258
263
  siteKey: opts.siteKey,
259
264
  endpoint: this.endpoint,
260
265
  debug: this.debug,
@@ -263,25 +268,27 @@ export class WebAnalytics {
263
268
  page: location.pathname,
264
269
  referrer: document.referrer || null,
265
270
  }),
266
- });
271
+ }).start();
272
+ this.log("error loaded");
267
273
  }
268
274
  if (
269
275
  opts.webVitals?.enabled ??
270
276
  opts.trackWebVitals ??
271
277
  hasWebVitalsSampling
272
278
  ) {
273
- this.load("web-vitals", "__FA_WebVitalsTracker", {
279
+ new WebVitalsTracker({
274
280
  siteKey: opts.siteKey,
275
281
  endpoint: this.endpoint,
276
282
  debug: this.debug,
277
283
  samplingPercentage: normalizeSamplingPercentage(
278
284
  opts.webVitals?.sampling?.percentage,
279
285
  ),
280
- });
286
+ }).start();
287
+ this.log("web-vitals loaded");
281
288
  }
282
289
  if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
283
290
  const replayOpts = opts.replayOptions ?? {};
284
- this.load("replay", "__FA_ReplayTracker", {
291
+ new ReplayTracker({
285
292
  siteKey: opts.siteKey,
286
293
  endpoint: this.endpoint,
287
294
  debug: this.debug,
@@ -290,7 +297,8 @@ export class WebAnalytics {
290
297
  replayOpts.samplingPercentage ??
291
298
  opts.sessionReplays?.sampling?.percentage,
292
299
  ),
293
- });
300
+ }).start();
301
+ this.log("replay loaded");
294
302
  }
295
303
 
296
304
  this.enterPage();
@@ -310,31 +318,6 @@ export class WebAnalytics {
310
318
  this.patch();
311
319
  }
312
320
 
313
- private load(
314
- script: string,
315
- key: string,
316
- trackerOpts: Record<string, unknown>,
317
- ): void {
318
- const el = document.createElement("script");
319
- el.src = `${this.baseUrl}/${script}.js`;
320
- el.async = true;
321
- el.onload = () => {
322
- const Ctor = (window as unknown as Record<string, unknown>)[key] as
323
- | (new (
324
- o: Record<string, unknown>,
325
- ) => { start(): void })
326
- | undefined;
327
- if (Ctor) {
328
- new Ctor(trackerOpts).start();
329
- this.log(`${script} loaded`);
330
- }
331
- };
332
- el.onerror = () => {
333
- if (this.debug) console.warn(`[Analytics] ${script} failed`);
334
- };
335
- document.head.appendChild(el);
336
- }
337
-
338
321
  pageview(extra: Dict = {}): void {
339
322
  const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
340
323
  if (key === this.pageKey) return;