@faststats/web 0.0.2 → 0.0.4

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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # @faststats/web
2
2
 
3
+ ## 0.0.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 93393c0: Remove redundant isTrackingDisabled checks from sub-trackers (ErrorTracker, WebVitalsTracker, ReplayTracker).
8
+ Simplify web vitals config - single source of truth (trackWebVitals ?? webVitals?.enabled ?? hasWebVitalsSampling).
9
+ Fix custom events via trackEvent: set window.\_\_FA_webAnalyticsInstance in init() so it works before async start() completes.
10
+
11
+ ## 0.0.3
12
+
13
+ ### Patch Changes
14
+
15
+ - bb3dcd1: ESM only for now
16
+
3
17
  ## 0.0.2
4
18
 
5
19
  ### Patch Changes
package/dist/module.d.ts CHANGED
@@ -429,7 +429,6 @@ interface IdentifyOptions {
429
429
  interface WebAnalyticsOptions {
430
430
  siteKey: string;
431
431
  endpoint?: string;
432
- baseUrl?: string;
433
432
  debug?: boolean;
434
433
  autoTrack?: boolean;
435
434
  trackHash?: boolean;
@@ -463,7 +462,6 @@ declare class WebAnalytics {
463
462
  private readonly options;
464
463
  private readonly endpoint;
465
464
  private readonly debug;
466
- private readonly baseUrl;
467
465
  private started;
468
466
  private pageKey;
469
467
  private navTimer;
@@ -481,7 +479,6 @@ declare class WebAnalytics {
481
479
  private log;
482
480
  private init;
483
481
  start(): void;
484
- private load;
485
482
  pageview(extra?: Dict): void;
486
483
  track(name: string, extra?: Dict): void;
487
484
  identify(externalId: string, email: string, options?: IdentifyOptions): void;
package/dist/module.js CHANGED
@@ -1,408 +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 trackEvent(eventName, properties) {
69
- if (typeof window === "undefined" || isTrackingDisabled()) return;
70
- window.__FA_webAnalyticsInstance?.track(eventName, properties ?? {});
71
- }
72
- function isTrackingDisabled() {
73
- if (typeof localStorage === "undefined") return false;
74
- const value = localStorage.getItem("disable-faststats");
75
- return value === "true" || value === "1";
76
- }
77
- async function sendData(options) {
78
- const { url, data, contentType = "application/json", headers = {}, debug = false, debugPrefix = "[Analytics]" } = options;
79
- const blob = data instanceof Blob ? data : new Blob([data], { type: contentType });
80
- if (navigator.sendBeacon?.(url, blob)) {
81
- if (debug) console.log(`${debugPrefix} ✓ Sent via beacon`);
82
- return true;
83
- }
84
- try {
85
- const response = await fetch(url, {
86
- method: "POST",
87
- body: data,
88
- headers: {
89
- "Content-Type": contentType,
90
- ...headers
91
- },
92
- keepalive: true
93
- });
94
- const success = response.ok;
95
- if (debug) if (success) console.log(`${debugPrefix} ✓ Sent via fetch`);
96
- else console.warn(`${debugPrefix} ✗ Failed: ${response.status}`);
97
- return success;
98
- } catch {
99
- if (debug) console.warn(`${debugPrefix} ✗ Failed to send`);
100
- return false;
101
- }
102
- }
103
- function getLinkEl(el) {
104
- while (el && !(el.tagName === "A" && el.href)) el = el.parentNode;
105
- return el;
106
- }
107
- function getUTM() {
108
- const params = {};
109
- if (!location.search) return params;
110
- const sp = new URLSearchParams(location.search);
111
- for (const k of [
112
- "utm_source",
113
- "utm_medium",
114
- "utm_campaign",
115
- "utm_term",
116
- "utm_content"
117
- ]) {
118
- const v = sp.get(k);
119
- if (v) params[k] = v;
120
- }
121
- return params;
122
- }
123
- var WebAnalytics = class {
124
- endpoint;
125
- debug;
126
- baseUrl;
127
- started = false;
128
- pageKey = "";
129
- navTimer = null;
130
- heartbeatTimer = null;
131
- scrollDepth = 0;
132
- pageEntryTime = 0;
133
- pagePath = "";
134
- pageUrl = "";
135
- pageHash = "";
136
- hasLeftCurrentPage = false;
137
- scrollHandler = null;
138
- consentMode;
139
- cookielessWhilePending;
140
- constructor(options) {
141
- this.options = options;
142
- this.endpoint = options.endpoint ?? "https://metrics.faststats.dev/v1/web";
143
- this.debug = options.debug ?? false;
144
- this.consentMode = options.consent?.mode ?? "granted";
145
- this.cookielessWhilePending = options.consent?.cookielessWhilePending ?? true;
146
- const script = typeof document !== "undefined" ? document.currentScript : null;
147
- const scriptBase = script?.src ? script.src.substring(0, script.src.lastIndexOf("/")) : "";
148
- this.baseUrl = (options.baseUrl ?? scriptBase) || (typeof window !== "undefined" ? window.location.origin : "");
149
- if (options.autoTrack ?? true) this.init();
150
- }
151
- log(msg) {
152
- if (this.debug) console.log(`[Analytics] ${msg}`);
153
- }
154
- init() {
155
- if (typeof window === "undefined") return;
156
- if (isTrackingDisabled()) {
157
- this.log("disabled");
158
- return;
159
- }
160
- if ("requestIdleCallback" in window) window.requestIdleCallback(() => this.start());
161
- else setTimeout(() => this.start(), 1);
162
- }
163
- start() {
164
- if (this.started || typeof window === "undefined") return;
165
- if (window.__FA_webAnalyticsInstance && window.__FA_webAnalyticsInstance !== this) {
166
- this.log("already started by another instance");
167
- return;
168
- }
169
- if (isTrackingDisabled()) {
170
- this.log("disabled");
171
- return;
172
- }
173
- this.started = true;
174
- window.__FA_webAnalyticsInstance = this;
175
- window.__FA_getAnonymousId = () => getAnonymousId(this.isCookielessMode());
176
- window.__FA_getSessionId = getOrCreateSessionId;
177
- window.__FA_sendData = sendData;
178
- window.__FA_identify = (externalId, email, options) => this.identify(externalId, email, options);
179
- window.__FA_logout = (resetAnonymousIdentity) => this.logout(resetAnonymousIdentity);
180
- window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
181
- window.__FA_optIn = () => this.optIn();
182
- window.__FA_optOut = () => this.optOut();
183
- window.__FA_trackEvent = (eventName, props) => this.track(eventName, props ?? {});
184
- window.__FA_isTrackingDisabled = isTrackingDisabled;
185
- const opts = this.options;
186
- const hasWebVitalsSampling = opts.webVitals?.sampling?.percentage !== void 0;
187
- const hasReplaySampling = opts.replayOptions?.samplingPercentage !== void 0 || opts.sessionReplays?.sampling?.percentage !== void 0;
188
- if (opts.errorTracking?.enabled ?? opts.trackErrors) this.load("error", "__FA_ErrorTracker", {
189
- siteKey: opts.siteKey,
190
- endpoint: this.endpoint,
191
- debug: this.debug,
192
- getCommonData: () => ({
193
- url: location.href,
194
- page: location.pathname,
195
- referrer: document.referrer || null
196
- })
197
- });
198
- if (opts.webVitals?.enabled ?? opts.trackWebVitals ?? hasWebVitalsSampling) this.load("web-vitals", "__FA_WebVitalsTracker", {
199
- siteKey: opts.siteKey,
200
- endpoint: this.endpoint,
201
- debug: this.debug,
202
- samplingPercentage: normalizeSamplingPercentage(opts.webVitals?.sampling?.percentage)
203
- });
204
- if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
205
- const replayOpts = opts.replayOptions ?? {};
206
- this.load("replay", "__FA_ReplayTracker", {
207
- siteKey: opts.siteKey,
208
- endpoint: this.endpoint,
209
- debug: this.debug,
210
- ...replayOpts,
211
- samplingPercentage: normalizeSamplingPercentage(replayOpts.samplingPercentage ?? opts.sessionReplays?.sampling?.percentage)
212
- });
213
- }
214
- this.enterPage();
215
- this.pageview({ trigger: "load" });
216
- this.links();
217
- this.trackScroll();
218
- this.startHeartbeat();
219
- document.addEventListener("visibilitychange", () => {
220
- if (document.visibilityState === "hidden") this.leavePage();
221
- else this.startHeartbeat();
222
- });
223
- window.addEventListener("pagehide", () => this.leavePage());
224
- window.addEventListener("popstate", () => this.navigate());
225
- if (opts.trackHash) window.addEventListener("hashchange", () => this.navigate());
226
- this.patch();
227
- }
228
- load(script, key, trackerOpts) {
229
- const el = document.createElement("script");
230
- el.src = `${this.baseUrl}/${script}.js`;
231
- el.async = true;
232
- el.onload = () => {
233
- const Ctor = window[key];
234
- if (Ctor) {
235
- new Ctor(trackerOpts).start();
236
- this.log(`${script} loaded`);
237
- }
238
- };
239
- el.onerror = () => {
240
- if (this.debug) console.warn(`[Analytics] ${script} failed`);
241
- };
242
- document.head.appendChild(el);
243
- }
244
- pageview(extra = {}) {
245
- const key = `${location.pathname}|${this.options.trackHash ?? false ? location.hash : ""}`;
246
- if (key === this.pageKey) return;
247
- this.pageKey = key;
248
- this.send("pageview", extra);
249
- }
250
- track(name, extra = {}) {
251
- this.send(name, extra);
252
- }
253
- identify(externalId, email, options = {}) {
254
- if (isTrackingDisabled()) return;
255
- if (this.isCookielessMode()) return;
256
- const trimmedExternalId = externalId.trim();
257
- const trimmedEmail = email.trim();
258
- if (!trimmedExternalId || !trimmedEmail) return;
259
- sendData({
260
- url: this.endpoint.replace(/\/v1\/web$/, "/v1/identify"),
261
- data: JSON.stringify({
262
- token: this.options.siteKey,
263
- identifier: getAnonymousId(false),
264
- externalId: trimmedExternalId,
265
- email: trimmedEmail,
266
- name: options.name?.trim() || void 0,
267
- phone: options.phone?.trim() || void 0,
268
- avatarUrl: options.avatarUrl?.trim() || void 0,
269
- traits: options.traits ?? {}
270
- }),
271
- contentType: "text/plain",
272
- debug: this.debug,
273
- debugPrefix: "[Analytics] identify"
274
- });
275
- }
276
- logout(resetAnonymousIdentity = true) {
277
- if (isTrackingDisabled()) return;
278
- if (resetAnonymousIdentity) resetAnonymousId(this.isCookielessMode());
279
- resetSessionId();
280
- }
281
- setConsentMode(mode) {
282
- this.consentMode = mode;
283
- }
284
- optIn() {
285
- this.setConsentMode("granted");
286
- }
287
- optOut() {
288
- this.setConsentMode("denied");
289
- }
290
- getConsentMode() {
291
- return this.consentMode;
292
- }
293
- isCookielessMode() {
294
- if (this.options.cookieless) return true;
295
- if (this.consentMode === "denied") return true;
296
- if (this.consentMode === "pending") return this.cookielessWhilePending;
297
- return false;
298
- }
299
- send(event, extra = {}) {
300
- const identifier = getAnonymousId(this.isCookielessMode());
301
- const payload = JSON.stringify({
302
- token: this.options.siteKey,
303
- ...identifier ? { userId: identifier } : {},
304
- sessionId: getOrCreateSessionId(),
305
- data: {
306
- event,
307
- page: location.pathname,
308
- referrer: document.referrer || null,
309
- title: document.title || "",
310
- url: location.href,
311
- ...getUTM(),
312
- ...extra
313
- }
314
- });
315
- this.log(event);
316
- sendData({
317
- url: this.endpoint,
318
- data: payload,
319
- contentType: "text/plain",
320
- debug: this.debug,
321
- debugPrefix: `[Analytics] ${event}`
322
- });
323
- }
324
- enterPage() {
325
- this.pageEntryTime = Date.now();
326
- this.pagePath = location.pathname;
327
- this.pageUrl = location.href;
328
- this.pageHash = location.hash;
329
- this.scrollDepth = 0;
330
- this.hasLeftCurrentPage = false;
331
- }
332
- leavePage() {
333
- if (this.hasLeftCurrentPage) return;
334
- this.hasLeftCurrentPage = true;
335
- const now = Date.now();
336
- this.send("page_leave", {
337
- page: this.pagePath,
338
- url: this.pageUrl,
339
- time_on_page: now - this.pageEntryTime,
340
- scroll_depth: this.scrollDepth,
341
- session_duration: now - getSessionStart()
342
- });
343
- }
344
- trackScroll() {
345
- if (this.scrollHandler) window.removeEventListener("scroll", this.scrollHandler);
346
- const update = () => {
347
- const doc = document.documentElement;
348
- const body = document.body;
349
- const viewportH = window.innerHeight;
350
- const docH = Math.max(doc.scrollHeight, body.scrollHeight);
351
- if (docH <= viewportH) {
352
- this.scrollDepth = 100;
353
- return;
354
- }
355
- const depth = Math.min(100, Math.round(((window.scrollY || doc.scrollTop) + viewportH) / docH * 100));
356
- if (depth > this.scrollDepth) this.scrollDepth = depth;
357
- };
358
- this.scrollHandler = update;
359
- update();
360
- window.addEventListener("scroll", update, { passive: true });
361
- }
362
- startHeartbeat() {
363
- if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
364
- this.heartbeatTimer = setInterval(() => {
365
- if (document.visibilityState === "hidden") {
366
- clearInterval(this.heartbeatTimer);
367
- this.heartbeatTimer = null;
368
- return;
369
- }
370
- refreshSessionTimestamp();
371
- }, 300 * 1e3);
372
- }
373
- navigate() {
374
- if (this.navTimer) clearTimeout(this.navTimer);
375
- this.navTimer = setTimeout(() => {
376
- this.navTimer = null;
377
- const pathChanged = location.pathname !== this.pagePath;
378
- const hashChanged = (this.options.trackHash ?? false) && location.hash !== this.pageHash;
379
- if (!pathChanged && !hashChanged) return;
380
- this.leavePage();
381
- this.enterPage();
382
- this.trackScroll();
383
- this.pageview({ trigger: "navigation" });
384
- }, 300);
385
- }
386
- patch() {
387
- const fire = () => this.navigate();
388
- for (const method of ["pushState", "replaceState"]) {
389
- const orig = history[method];
390
- history[method] = function(...args) {
391
- const result = orig.apply(this, args);
392
- fire();
393
- return result;
394
- };
395
- }
396
- }
397
- links() {
398
- const handler = (event) => {
399
- const link = getLinkEl(event.target);
400
- if (link && link.host !== location.host) this.track("outbound_link", { outbound_link: link.href });
401
- };
402
- document.addEventListener("click", handler);
403
- document.addEventListener("auxclick", handler);
404
- }
405
- };
406
-
407
- //#endregion
408
- export { WebAnalytics, trackEvent };
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(){this.started||typeof window>`u`||(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`||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(){this.started||typeof window>`u`||(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}window.__FA_webAnalyticsInstance=this,`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.trackWebVitals??e.webVitals?.enabled??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
@@ -15,11 +15,10 @@
15
15
  "publishConfig": {
16
16
  "access": "public"
17
17
  },
18
- "version": "0.0.2",
18
+ "version": "0.0.4",
19
19
  "scripts": {
20
- "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",
21
- "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",
22
- "dev": "bun run build && bun run sync:example-assets",
20
+ "build": "tsdown",
21
+ "dev": "bun run build",
23
22
  "deploy:worker": "wrangler deploy"
24
23
  },
25
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,6 +13,7 @@ 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
 
@@ -139,7 +142,6 @@ export interface IdentifyOptions {
139
142
  export interface WebAnalyticsOptions {
140
143
  siteKey: string;
141
144
  endpoint?: string;
142
- baseUrl?: string;
143
145
  debug?: boolean;
144
146
  autoTrack?: boolean;
145
147
  trackHash?: boolean;
@@ -170,7 +172,10 @@ declare global {
170
172
  __FA_optIn?: () => void;
171
173
  __FA_optOut?: () => void;
172
174
  __FA_isTrackingDisabled?: () => boolean;
173
- __FA_trackEvent?: (eventName: string, properties?: Record<string, unknown>) => void;
175
+ __FA_trackEvent?: (
176
+ eventName: string,
177
+ properties?: Record<string, unknown>,
178
+ ) => void;
174
179
  __FA_webAnalyticsInstance?: WebAnalytics;
175
180
  }
176
181
  }
@@ -178,7 +183,6 @@ declare global {
178
183
  export class WebAnalytics {
179
184
  private readonly endpoint: string;
180
185
  private readonly debug: boolean;
181
- private readonly baseUrl: string;
182
186
  private started = false;
183
187
  private pageKey = "";
184
188
  private navTimer: ReturnType<typeof setTimeout> | null = null;
@@ -199,16 +203,6 @@ export class WebAnalytics {
199
203
  this.consentMode = options.consent?.mode ?? "granted";
200
204
  this.cookielessWhilePending =
201
205
  options.consent?.cookielessWhilePending ?? true;
202
- const script =
203
- typeof document !== "undefined"
204
- ? (document.currentScript as HTMLScriptElement | null)
205
- : null;
206
- const scriptBase = script?.src
207
- ? script.src.substring(0, script.src.lastIndexOf("/"))
208
- : "";
209
- this.baseUrl =
210
- (options.baseUrl ?? scriptBase) ||
211
- (typeof window !== "undefined" ? window.location.origin : "");
212
206
  if (options.autoTrack ?? true) this.init();
213
207
  }
214
208
 
@@ -222,6 +216,7 @@ export class WebAnalytics {
222
216
  this.log("disabled");
223
217
  return;
224
218
  }
219
+ window.__FA_webAnalyticsInstance = this;
225
220
  if ("requestIdleCallback" in window)
226
221
  window.requestIdleCallback(() => this.start());
227
222
  else setTimeout(() => this.start(), 1);
@@ -265,7 +260,7 @@ export class WebAnalytics {
265
260
  opts.sessionReplays?.sampling?.percentage !== undefined;
266
261
 
267
262
  if (opts.errorTracking?.enabled ?? opts.trackErrors) {
268
- this.load("error", "__FA_ErrorTracker", {
263
+ new ErrorTracker({
269
264
  siteKey: opts.siteKey,
270
265
  endpoint: this.endpoint,
271
266
  debug: this.debug,
@@ -274,25 +269,27 @@ export class WebAnalytics {
274
269
  page: location.pathname,
275
270
  referrer: document.referrer || null,
276
271
  }),
277
- });
272
+ }).start();
273
+ this.log("error loaded");
278
274
  }
279
275
  if (
280
- opts.webVitals?.enabled ??
281
276
  opts.trackWebVitals ??
277
+ opts.webVitals?.enabled ??
282
278
  hasWebVitalsSampling
283
279
  ) {
284
- this.load("web-vitals", "__FA_WebVitalsTracker", {
280
+ new WebVitalsTracker({
285
281
  siteKey: opts.siteKey,
286
282
  endpoint: this.endpoint,
287
283
  debug: this.debug,
288
284
  samplingPercentage: normalizeSamplingPercentage(
289
285
  opts.webVitals?.sampling?.percentage,
290
286
  ),
291
- });
287
+ }).start();
288
+ this.log("web-vitals loaded");
292
289
  }
293
290
  if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
294
291
  const replayOpts = opts.replayOptions ?? {};
295
- this.load("replay", "__FA_ReplayTracker", {
292
+ new ReplayTracker({
296
293
  siteKey: opts.siteKey,
297
294
  endpoint: this.endpoint,
298
295
  debug: this.debug,
@@ -301,7 +298,8 @@ export class WebAnalytics {
301
298
  replayOpts.samplingPercentage ??
302
299
  opts.sessionReplays?.sampling?.percentage,
303
300
  ),
304
- });
301
+ }).start();
302
+ this.log("replay loaded");
305
303
  }
306
304
 
307
305
  this.enterPage();
@@ -321,31 +319,6 @@ export class WebAnalytics {
321
319
  this.patch();
322
320
  }
323
321
 
324
- private load(
325
- script: string,
326
- key: string,
327
- trackerOpts: Record<string, unknown>,
328
- ): void {
329
- const el = document.createElement("script");
330
- el.src = `${this.baseUrl}/${script}.js`;
331
- el.async = true;
332
- el.onload = () => {
333
- const Ctor = (window as unknown as Record<string, unknown>)[key] as
334
- | (new (
335
- o: Record<string, unknown>,
336
- ) => { start(): void })
337
- | undefined;
338
- if (Ctor) {
339
- new Ctor(trackerOpts).start();
340
- this.log(`${script} loaded`);
341
- }
342
- };
343
- el.onerror = () => {
344
- if (this.debug) console.warn(`[Analytics] ${script} failed`);
345
- };
346
- document.head.appendChild(el);
347
- }
348
-
349
322
  pageview(extra: Dict = {}): void {
350
323
  const key = `${location.pathname}|${(this.options.trackHash ?? false) ? location.hash : ""}`;
351
324
  if (key === this.pageKey) return;
package/src/error.ts CHANGED
@@ -57,12 +57,6 @@ class ErrorTracker {
57
57
 
58
58
  start(): void {
59
59
  if (this.started || typeof window === "undefined") return;
60
- if (window.__FA_isTrackingDisabled?.()) {
61
- if (this.debug) {
62
- console.log("[ErrorTracker] Tracking disabled via localStorage");
63
- }
64
- return;
65
- }
66
60
  this.started = true;
67
61
 
68
62
  window.addEventListener("error", this.handleErrorEvent);