@faststats/web 0.0.4 → 0.1.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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # @faststats/web
2
2
 
3
+ ## 0.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - 2326d00: fix: some cleanup
8
+
9
+ ## 0.1.0
10
+
11
+ ### Minor Changes
12
+
13
+ - c63d783: Remove all window globals; use module-level instance and dependency injection. Add dynamic imports for ErrorTracker, WebVitalsTracker, ReplayTracker (tree-shakable). Export identify, logout, setConsentMode, optIn, optOut, getInstance from @faststats/web. React package now uses getInstance instead of window.
14
+
3
15
  ## 0.0.4
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1,3 @@
1
+ var e=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;getCommonData;getAnonymousId;getSessionId;sendData;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??(()=>({})),this.getAnonymousId=e.getAnonymousId??(()=>``),this.getSessionId=e.getSessionId??(()=>``),this.sendData=e.sendData??(async()=>!1)}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=this.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:this.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),this.sendData({url:this.endpoint,data:o,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{e as default};
package/dist/module.d.ts CHANGED
@@ -381,25 +381,21 @@ interface ReplayTrackerOptions {
381
381
  checkoutEveryNms?: number;
382
382
  checkoutEveryNth?: number;
383
383
  recordConsole?: boolean;
384
- }
385
- type ReplayTrackerPublicInstance = {
386
- start(): void;
387
- stop(): void;
388
- getSessionId(): string | undefined;
389
- };
390
- type ReplayTrackerConstructor = new (options: ReplayTrackerOptions) => ReplayTrackerPublicInstance;
391
- declare global {
392
- interface Window {
393
- __FA_ReplayTracker?: ReplayTrackerConstructor;
394
- __FA_getAnonymousId?: () => string;
395
- __FA_getSessionId?: () => string;
396
- __FA_sendData?: (options: SendDataOptions) => Promise<boolean>;
397
- __FA_isTrackingDisabled?: () => boolean;
398
- }
384
+ getAnonymousId?: () => string;
385
+ getSessionId?: () => string;
386
+ sendData?: (options: SendDataOptions) => Promise<boolean>;
399
387
  }
400
388
  //#endregion
401
389
  //#region src/analytics.d.ts
390
+ declare function getInstance(): WebAnalytics | null;
402
391
  declare function trackEvent(eventName: string, properties?: Record<string, unknown>): void;
392
+ declare function identify(externalId: string, email: string, options?: IdentifyOptions): void;
393
+ declare function logout(resetAnonymousIdentity?: boolean): void;
394
+ declare function setConsentMode(mode: ConsentMode): void;
395
+ declare function optIn(): void;
396
+ declare function optOut(): void;
397
+ declare function isTrackingDisabled(): boolean;
398
+ declare function sendData(options: SendDataOptions): Promise<boolean>;
403
399
  type Dict = Record<string, unknown>;
404
400
  interface SamplingOptions {
405
401
  percentage?: number;
@@ -408,11 +404,9 @@ interface ErrorTrackingConfig {
408
404
  enabled?: boolean;
409
405
  }
410
406
  interface WebVitalsConfig {
411
- enabled?: boolean;
412
407
  sampling?: SamplingOptions;
413
408
  }
414
409
  interface SessionReplayConfig {
415
- enabled?: boolean;
416
410
  sampling?: SamplingOptions;
417
411
  }
418
412
  type ConsentMode = "pending" | "granted" | "denied";
@@ -442,22 +436,6 @@ interface WebAnalyticsOptions {
442
436
  sessionReplays?: SessionReplayConfig;
443
437
  replayOptions?: Partial<ReplayTrackerOptions>;
444
438
  }
445
- declare global {
446
- interface Window {
447
- WebAnalytics: typeof WebAnalytics;
448
- __FA_getAnonymousId?: () => string;
449
- __FA_getSessionId?: () => string;
450
- __FA_sendData?: (options: SendDataOptions) => Promise<boolean>;
451
- __FA_identify?: (externalId: string, email: string, options?: IdentifyOptions) => void;
452
- __FA_logout?: (resetAnonymousIdentity?: boolean) => void;
453
- __FA_setConsentMode?: (mode: ConsentMode) => void;
454
- __FA_optIn?: () => void;
455
- __FA_optOut?: () => void;
456
- __FA_isTrackingDisabled?: () => boolean;
457
- __FA_trackEvent?: (eventName: string, properties?: Record<string, unknown>) => void;
458
- __FA_webAnalyticsInstance?: WebAnalytics;
459
- }
460
- }
461
439
  declare class WebAnalytics {
462
440
  private readonly options;
463
441
  private readonly endpoint;
@@ -478,7 +456,7 @@ declare class WebAnalytics {
478
456
  constructor(options: WebAnalyticsOptions);
479
457
  private log;
480
458
  private init;
481
- start(): void;
459
+ start(): Promise<void>;
482
460
  pageview(extra?: Dict): void;
483
461
  track(name: string, extra?: Dict): void;
484
462
  identify(externalId: string, email: string, options?: IdentifyOptions): void;
@@ -487,6 +465,8 @@ declare class WebAnalytics {
487
465
  optIn(): void;
488
466
  optOut(): void;
489
467
  getConsentMode(): ConsentMode;
468
+ getAnonymousId(): string;
469
+ getSessionId(): string;
490
470
  private isCookielessMode;
491
471
  private send;
492
472
  private enterPage;
@@ -498,4 +478,4 @@ declare class WebAnalytics {
498
478
  private links;
499
479
  }
500
480
  //#endregion
501
- export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions, trackEvent };
481
+ export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions, getInstance, identify, isTrackingDisabled, logout, optIn, optOut, sendData, setConsentMode, trackEvent };
package/dist/module.js CHANGED
@@ -1,3 +1 @@
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};
1
+ import{t as e}from"./types-E07i1PxI.js";function t(e){return e?``:n()}function n(){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 r(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),n())}function i(){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 a(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),i())}function o(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function s(){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()}let c=null;function l(){return c}function u(e,t){typeof window>`u`||g()||c?.track(e,t??{})}function d(e,t,n){typeof window>`u`||g()||c?.identify(e,t,n??{})}function f(e=!0){typeof window>`u`||g()||c?.logout(e)}function p(e){c?.setConsentMode(e)}function m(){p(`granted`)}function h(){p(`denied`)}function g(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function _(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 v(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function y(){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 b=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(g()){this.log(`disabled`);return}c=this,setTimeout(()=>void this.start(),0)}}async start(){if(this.started||typeof window>`u`)return;if(c&&c!==this){this.log(`already started by another instance`);return}if(g()){this.log(`disabled`);return}this.started=!0,c=this;let n=()=>t(this.isCookielessMode()),r=this.options;if(r.errorTracking?.enabled??r.trackErrors){let{default:e}=await import(`./error-DtTBFJh_.js`);new e({siteKey:r.siteKey,endpoint:this.endpoint,debug:this.debug,getCommonData:()=>({url:location.href,page:location.pathname,referrer:document.referrer||null}),getAnonymousId:n,getSessionId:i,sendData:_}).start(),this.log(`error loaded`)}if(r.trackWebVitals){let{default:t}=await import(`./web-vitals-CUXDm9A3.js`);new t({siteKey:r.siteKey,endpoint:this.endpoint,debug:this.debug,samplingPercentage:e(r.webVitals?.sampling?.percentage),getSessionId:i}).start(),this.log(`web-vitals loaded`)}if(r.trackReplay){let{default:e}=await import(`./replay-ZmimkIBT.js`);new e({siteKey:r.siteKey,endpoint:this.endpoint,debug:this.debug,getAnonymousId:n,getSessionId:i,sendData:_}).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()),r.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,n,r={}){if(g()||this.isCookielessMode())return;let i=e.trim(),a=n.trim();!i||!a||_({url:this.endpoint.replace(/\/v1\/web$/,`/v1/identify`),data:JSON.stringify({token:this.options.siteKey,identifier:t(!1),externalId:i,email:a,name:r.name?.trim()||void 0,phone:r.phone?.trim()||void 0,avatarUrl:r.avatarUrl?.trim()||void 0,traits:r.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){g()||(e&&r(this.isCookielessMode()),a())}setConsentMode(e){this.consentMode=e}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return t(this.isCookielessMode())}getSessionId(){return i()}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,n={}){let r=t(this.isCookielessMode()),a=JSON.stringify({token:this.options.siteKey,...r?{userId:r}:{},sessionId:i(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...y(),...n}});this.log(e),_({url:this.endpoint,data:a,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-s()})}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}o()},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=v(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};document.addEventListener(`click`,e),document.addEventListener(`auxclick`,e)}};export{b as WebAnalytics,l as getInstance,d as identify,g as isTrackingDisabled,f as logout,m as optIn,h as optOut,_ as sendData,p as setConsentMode,u as trackEvent};
@@ -0,0 +1 @@
1
+ import{t as e}from"./types-E07i1PxI.js";import{getRecordConsolePlugin as t}from"@rrweb/rrweb-plugin-console-record";import{record as n}from"rrweb";var r=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;_getAnonymousId;_getSessionId;sendData;constructor(t){this.siteKey=t.siteKey,this.endpoint=t.endpoint?.replace(/\/v1\/web$/,`/v1/replay`)??`https://metrics.faststats.dev/v1/replay`,this.debug=t.debug??!1,this.samplingPercentage=e(t.samplingPercentage),this.flushInterval=t.flushInterval??1e4,this.maxEvents=t.maxEvents??500,this.sampling=t.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=t.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=t.maskAllInputs??!0,this.maskInputOptions=t.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=t.blockClass,this.blockSelector=t.blockSelector,this.maskTextClass=t.maskTextClass,this.maskTextSelector=t.maskTextSelector,this.checkoutEveryNms=t.checkoutEveryNms??6e4,this.checkoutEveryNth=t.checkoutEveryNth,this.recordConsole=t.recordConsole??!0,this.sessionSamplingSeed=Math.random()*100,this._getAnonymousId=t.getAnonymousId??(()=>``),this._getSessionId=t.getSessionId??(()=>``),this.sendData=t.sendData??(async()=>!1),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 e={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(e.maskInputOptions=this.maskInputOptions),this.blockClass&&(e.blockClass=this.blockClass),this.blockSelector&&(e.blockSelector=this.blockSelector),this.maskTextClass&&(e.maskTextClass=this.maskTextClass),this.maskTextSelector&&(e.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(e.checkoutEveryNth=this.checkoutEveryNth),this.recordConsole&&(e.plugins=[t()]),this.stopRecording=n(e),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=this._getAnonymousId(),r={token:this.siteKey,sessionId:this._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 this.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 this._getSessionId()}};export{r as default};
@@ -0,0 +1 @@
1
+ function e(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{e as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./types-E07i1PxI.js";import{onCLS as t,onFCP as n,onINP as r,onLCP as i,onTTFB as a}from"web-vitals/attribution";var o=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;getSessionId;constructor(t){this.siteKey=t.siteKey,this.endpoint=this.getVitalsEndpoint(t.endpoint??`https://metrics.faststats.dev/v1/web`),this.debug=t.debug??!1,this.samplingPercentage=e(t.samplingPercentage),this.sessionSamplingSeed=Math.random()*100,this.getSessionId=t.getSessionId??(()=>``)}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`),t(e=>this.captureMetric(e)),r(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),n(e=>this.captureMetric(e)),a(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:this.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`)})}}};export{o as default};
package/package.json CHANGED
@@ -1,6 +1,9 @@
1
1
  {
2
2
  "name": "@faststats/web",
3
- "repository": "https://github.com/faststats-dev/web-analytics",
3
+ "repository": {
4
+ "type": "git",
5
+ "url": "https://github.com/faststats-dev/web-analytics.git"
6
+ },
4
7
  "main": "dist/module.js",
5
8
  "module": "dist/module.js",
6
9
  "types": "dist/module.d.ts",
@@ -12,10 +15,11 @@
12
15
  }
13
16
  },
14
17
  "type": "module",
18
+ "sideEffects": true,
15
19
  "publishConfig": {
16
20
  "access": "public"
17
21
  },
18
- "version": "0.0.4",
22
+ "version": "0.1.1",
19
23
  "scripts": {
20
24
  "build": "tsdown",
21
25
  "dev": "bun run build",
package/src/analytics.ts CHANGED
@@ -1,6 +1,4 @@
1
- import ErrorTracker from "./error";
2
1
  import type { ReplayTrackerOptions } from "./replay";
3
- import ReplayTracker from "./replay";
4
2
  import {
5
3
  getAnonymousId,
6
4
  getOrCreateSessionId,
@@ -13,16 +11,47 @@ import {
13
11
  normalizeSamplingPercentage,
14
12
  type SendDataOptions,
15
13
  } from "./utils/types";
16
- import WebVitalsTracker from "./web-vitals";
17
14
 
18
15
  export type { SendDataOptions };
19
16
 
17
+ let _instance: WebAnalytics | null = null;
18
+
19
+ export function getInstance(): WebAnalytics | null {
20
+ return _instance;
21
+ }
22
+
20
23
  export function trackEvent(
21
24
  eventName: string,
22
25
  properties?: Record<string, unknown>,
23
26
  ): void {
24
27
  if (typeof window === "undefined" || isTrackingDisabled()) return;
25
- window.__FA_webAnalyticsInstance?.track(eventName, properties ?? {});
28
+ _instance?.track(eventName, properties ?? {});
29
+ }
30
+
31
+ export function identify(
32
+ externalId: string,
33
+ email: string,
34
+ options?: IdentifyOptions,
35
+ ): void {
36
+ if (typeof window === "undefined" || isTrackingDisabled()) return;
37
+ _instance?.identify(externalId, email, options ?? {});
38
+ }
39
+
40
+ export function logout(resetAnonymousIdentity = true): void {
41
+ if (typeof window === "undefined" || isTrackingDisabled()) return;
42
+ _instance?.logout(resetAnonymousIdentity);
43
+ }
44
+
45
+ export function setConsentMode(mode: ConsentMode): void {
46
+ _instance?.setConsentMode(mode);
47
+ }
48
+
49
+ export function optIn(): void {
50
+ setConsentMode("granted");
51
+ }
52
+
53
+ export function optOut(): void {
54
+ setConsentMode("denied");
26
55
  }
27
56
 
28
57
  export function isTrackingDisabled(): boolean {
@@ -79,13 +108,11 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
79
108
  }
80
109
 
81
110
  function getLinkEl(el: Node | null): HTMLAnchorElement | null {
82
- while (
83
- el &&
84
- !((el as Element).tagName === "A" && (el as HTMLAnchorElement).href)
85
- ) {
111
+ while (el) {
112
+ if (el instanceof HTMLAnchorElement && el.href) return el;
86
113
  el = el.parentNode;
87
114
  }
88
- return el as HTMLAnchorElement | null;
115
+ return null;
89
116
  }
90
117
 
91
118
  function getUTM(): Record<string, string> {
@@ -116,12 +143,10 @@ interface ErrorTrackingConfig {
116
143
  }
117
144
 
118
145
  interface WebVitalsConfig {
119
- enabled?: boolean;
120
146
  sampling?: SamplingOptions;
121
147
  }
122
148
 
123
149
  interface SessionReplayConfig {
124
- enabled?: boolean;
125
150
  sampling?: SamplingOptions;
126
151
  }
127
152
 
@@ -156,30 +181,6 @@ export interface WebAnalyticsOptions {
156
181
  replayOptions?: Partial<ReplayTrackerOptions>;
157
182
  }
158
183
 
159
- declare global {
160
- interface Window {
161
- WebAnalytics: typeof WebAnalytics;
162
- __FA_getAnonymousId?: () => string;
163
- __FA_getSessionId?: () => string;
164
- __FA_sendData?: (options: SendDataOptions) => Promise<boolean>;
165
- __FA_identify?: (
166
- externalId: string,
167
- email: string,
168
- options?: IdentifyOptions,
169
- ) => void;
170
- __FA_logout?: (resetAnonymousIdentity?: boolean) => void;
171
- __FA_setConsentMode?: (mode: ConsentMode) => void;
172
- __FA_optIn?: () => void;
173
- __FA_optOut?: () => void;
174
- __FA_isTrackingDisabled?: () => boolean;
175
- __FA_trackEvent?: (
176
- eventName: string,
177
- properties?: Record<string, unknown>,
178
- ) => void;
179
- __FA_webAnalyticsInstance?: WebAnalytics;
180
- }
181
- }
182
-
183
184
  export class WebAnalytics {
184
185
  private readonly endpoint: string;
185
186
  private readonly debug: boolean;
@@ -216,18 +217,13 @@ export class WebAnalytics {
216
217
  this.log("disabled");
217
218
  return;
218
219
  }
219
- window.__FA_webAnalyticsInstance = this;
220
- if ("requestIdleCallback" in window)
221
- window.requestIdleCallback(() => this.start());
222
- else setTimeout(() => this.start(), 1);
220
+ _instance = this;
221
+ setTimeout(() => void this.start(), 0);
223
222
  }
224
223
 
225
- start(): void {
224
+ async start(): Promise<void> {
226
225
  if (this.started || typeof window === "undefined") return;
227
- if (
228
- window.__FA_webAnalyticsInstance &&
229
- window.__FA_webAnalyticsInstance !== this
230
- ) {
226
+ if (_instance && _instance !== this) {
231
227
  this.log("already started by another instance");
232
228
  return;
233
229
  }
@@ -236,30 +232,13 @@ export class WebAnalytics {
236
232
  return;
237
233
  }
238
234
  this.started = true;
239
- window.__FA_webAnalyticsInstance = this;
240
-
241
- window.__FA_getAnonymousId = () => getAnonymousId(this.isCookielessMode());
242
- window.__FA_getSessionId = getOrCreateSessionId;
243
- window.__FA_sendData = sendData;
244
- window.__FA_identify = (externalId, email, options) =>
245
- this.identify(externalId, email, options);
246
- window.__FA_logout = (resetAnonymousIdentity) =>
247
- this.logout(resetAnonymousIdentity);
248
- window.__FA_setConsentMode = (mode) => this.setConsentMode(mode);
249
- window.__FA_optIn = () => this.optIn();
250
- window.__FA_optOut = () => this.optOut();
251
- window.__FA_trackEvent = (eventName, props) =>
252
- this.track(eventName, props ?? {});
253
- window.__FA_isTrackingDisabled = isTrackingDisabled;
235
+ _instance = this;
254
236
 
237
+ const getAnonymousIdFn = () => getAnonymousId(this.isCookielessMode());
255
238
  const opts = this.options;
256
- const hasWebVitalsSampling =
257
- opts.webVitals?.sampling?.percentage !== undefined;
258
- const hasReplaySampling =
259
- opts.replayOptions?.samplingPercentage !== undefined ||
260
- opts.sessionReplays?.sampling?.percentage !== undefined;
261
239
 
262
240
  if (opts.errorTracking?.enabled ?? opts.trackErrors) {
241
+ const { default: ErrorTracker } = await import("./error");
263
242
  new ErrorTracker({
264
243
  siteKey: opts.siteKey,
265
244
  endpoint: this.endpoint,
@@ -269,14 +248,14 @@ export class WebAnalytics {
269
248
  page: location.pathname,
270
249
  referrer: document.referrer || null,
271
250
  }),
251
+ getAnonymousId: getAnonymousIdFn,
252
+ getSessionId: getOrCreateSessionId,
253
+ sendData,
272
254
  }).start();
273
255
  this.log("error loaded");
274
256
  }
275
- if (
276
- opts.trackWebVitals ??
277
- opts.webVitals?.enabled ??
278
- hasWebVitalsSampling
279
- ) {
257
+ if (opts.trackWebVitals) {
258
+ const { default: WebVitalsTracker } = await import("./web-vitals");
280
259
  new WebVitalsTracker({
281
260
  siteKey: opts.siteKey,
282
261
  endpoint: this.endpoint,
@@ -284,20 +263,19 @@ export class WebAnalytics {
284
263
  samplingPercentage: normalizeSamplingPercentage(
285
264
  opts.webVitals?.sampling?.percentage,
286
265
  ),
266
+ getSessionId: getOrCreateSessionId,
287
267
  }).start();
288
268
  this.log("web-vitals loaded");
289
269
  }
290
- if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
291
- const replayOpts = opts.replayOptions ?? {};
270
+ if (opts.trackReplay) {
271
+ const { default: ReplayTracker } = await import("./replay");
292
272
  new ReplayTracker({
293
273
  siteKey: opts.siteKey,
294
274
  endpoint: this.endpoint,
295
275
  debug: this.debug,
296
- ...replayOpts,
297
- samplingPercentage: normalizeSamplingPercentage(
298
- replayOpts.samplingPercentage ??
299
- opts.sessionReplays?.sampling?.percentage,
300
- ),
276
+ getAnonymousId: getAnonymousIdFn,
277
+ getSessionId: getOrCreateSessionId,
278
+ sendData,
301
279
  }).start();
302
280
  this.log("replay loaded");
303
281
  }
@@ -390,6 +368,14 @@ export class WebAnalytics {
390
368
  return this.consentMode;
391
369
  }
392
370
 
371
+ getAnonymousId(): string {
372
+ return getAnonymousId(this.isCookielessMode());
373
+ }
374
+
375
+ getSessionId(): string {
376
+ return getOrCreateSessionId();
377
+ }
378
+
393
379
  private isCookielessMode(): boolean {
394
380
  if (this.options.cookieless) return true;
395
381
  if (this.consentMode === "denied") return true;
package/src/error.ts CHANGED
@@ -28,6 +28,9 @@ export interface ErrorTrackingOptions {
28
28
  flushInterval?: number;
29
29
  maxQueueSize?: number;
30
30
  getCommonData?: () => Record<string, unknown>;
31
+ getAnonymousId?: () => string;
32
+ getSessionId?: () => string;
33
+ sendData?: (options: SendDataOptions) => Promise<boolean>;
31
34
  }
32
35
 
33
36
  class ErrorTracker {
@@ -37,6 +40,9 @@ class ErrorTracker {
37
40
  private readonly flushInterval: number;
38
41
  private readonly maxQueueSize: number;
39
42
  private readonly getCommonData: () => Record<string, unknown>;
43
+ private readonly getAnonymousId: () => string;
44
+ private readonly getSessionId: () => string;
45
+ private readonly sendData: (options: SendDataOptions) => Promise<boolean>;
40
46
 
41
47
  private readonly handledErrors = new WeakSet<Error>();
42
48
  private readonly errorCounts = new Map<
@@ -53,6 +59,9 @@ class ErrorTracker {
53
59
  this.flushInterval = options.flushInterval ?? 5000;
54
60
  this.maxQueueSize = options.maxQueueSize ?? 50;
55
61
  this.getCommonData = options.getCommonData ?? (() => ({}));
62
+ this.getAnonymousId = options.getAnonymousId ?? (() => "");
63
+ this.getSessionId = options.getSessionId ?? (() => "");
64
+ this.sendData = options.sendData ?? (async () => false);
56
65
  }
57
66
 
58
67
  start(): void {
@@ -260,7 +269,7 @@ class ErrorTracker {
260
269
  }
261
270
 
262
271
  const commonData = this.getCommonData();
263
- const identifier = window.__FA_getAnonymousId?.();
272
+ const identifier = this.getAnonymousId();
264
273
  const sourcemapsBuild = (
265
274
  globalThis as {
266
275
  __SOURCEMAPS_BUILD__?: { buildId?: unknown };
@@ -275,7 +284,7 @@ class ErrorTracker {
275
284
  const payload: Record<string, unknown> = {
276
285
  token: this.siteKey,
277
286
  ...(identifier ? { userId: identifier } : {}),
278
- sessionId: window.__FA_getSessionId?.(),
287
+ sessionId: this.getSessionId(),
279
288
  ...(buildId ? { buildId } : {}),
280
289
  data: {
281
290
  url: commonData.url as string,
@@ -292,7 +301,7 @@ class ErrorTracker {
292
301
  console.log("[ErrorTracker] Payload:", payloadString);
293
302
  }
294
303
 
295
- window.__FA_sendData?.({
304
+ this.sendData({
296
305
  url: this.endpoint,
297
306
  data: payloadString,
298
307
  debug: this.debug,
@@ -302,17 +311,3 @@ class ErrorTracker {
302
311
  }
303
312
 
304
313
  export default ErrorTracker;
305
-
306
- declare global {
307
- interface Window {
308
- __FA_ErrorTracker?: typeof ErrorTracker;
309
- __FA_getAnonymousId?: () => string;
310
- __FA_getSessionId?: () => string;
311
- __FA_sendData?: (options: SendDataOptions) => Promise<boolean>;
312
- __FA_isTrackingDisabled?: () => boolean;
313
- }
314
- }
315
-
316
- if (typeof window !== "undefined") {
317
- window.__FA_ErrorTracker = ErrorTracker;
318
- }
package/src/module.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  export {
2
2
  type ConsentMode,
3
+ getInstance,
3
4
  type IdentifyOptions,
5
+ identify,
6
+ isTrackingDisabled,
7
+ logout,
8
+ optIn,
9
+ optOut,
10
+ sendData,
11
+ setConsentMode,
4
12
  trackEvent,
5
13
  WebAnalytics,
6
14
  type WebAnalyticsOptions,
package/src/replay.ts CHANGED
@@ -26,18 +26,11 @@ export interface ReplayTrackerOptions {
26
26
  checkoutEveryNms?: number;
27
27
  checkoutEveryNth?: number;
28
28
  recordConsole?: boolean;
29
+ getAnonymousId?: () => string;
30
+ getSessionId?: () => string;
31
+ sendData?: (options: SendDataOptions) => Promise<boolean>;
29
32
  }
30
33
 
31
- type ReplayTrackerPublicInstance = {
32
- start(): void;
33
- stop(): void;
34
- getSessionId(): string | undefined;
35
- };
36
-
37
- type ReplayTrackerConstructor = new (
38
- options: ReplayTrackerOptions,
39
- ) => ReplayTrackerPublicInstance;
40
-
41
34
  interface PendingBatch {
42
35
  batch: {
43
36
  token: string;
@@ -81,6 +74,9 @@ class ReplayTracker {
81
74
  private compressionSupported = false;
82
75
  private readonly sessionSamplingSeed: number;
83
76
  private readonly minReplayLengthMs = 3000;
77
+ private readonly _getAnonymousId: () => string;
78
+ private readonly _getSessionId: () => string;
79
+ private readonly sendData: (options: SendDataOptions) => Promise<boolean>;
84
80
 
85
81
  constructor(options: ReplayTrackerOptions) {
86
82
  this.siteKey = options.siteKey;
@@ -125,6 +121,9 @@ class ReplayTracker {
125
121
  this.checkoutEveryNth = options.checkoutEveryNth;
126
122
  this.recordConsole = options.recordConsole ?? true;
127
123
  this.sessionSamplingSeed = Math.random() * 100;
124
+ this._getAnonymousId = options.getAnonymousId ?? (() => "");
125
+ this._getSessionId = options.getSessionId ?? (() => "");
126
+ this.sendData = options.sendData ?? (async () => false);
128
127
 
129
128
  if (typeof window !== "undefined") {
130
129
  this.compressionSupported = "CompressionStream" in window;
@@ -286,10 +285,10 @@ class ReplayTracker {
286
285
  const eventsToSend = this.events;
287
286
  this.events = [];
288
287
 
289
- const anonymousId = window.__FA_getAnonymousId?.();
288
+ const anonymousId = this._getAnonymousId();
290
289
  const batch = {
291
290
  token: this.siteKey,
292
- sessionId: window.__FA_getSessionId?.(),
291
+ sessionId: this._getSessionId(),
293
292
  ...(anonymousId ? { identifier: anonymousId } : {}),
294
293
  sequence: this.sequenceNumber++,
295
294
  timestamp: Date.now(),
@@ -447,7 +446,7 @@ class ReplayTracker {
447
446
  const sizeKB = (data.size / 1024).toFixed(1);
448
447
 
449
448
  return (
450
- window.__FA_sendData?.({
449
+ this.sendData({
451
450
  url,
452
451
  data,
453
452
  contentType: isCompressed
@@ -461,22 +460,8 @@ class ReplayTracker {
461
460
  }
462
461
 
463
462
  getSessionId(): string | undefined {
464
- return window.__FA_getSessionId?.();
463
+ return this._getSessionId();
465
464
  }
466
465
  }
467
466
 
468
467
  export default ReplayTracker;
469
-
470
- declare global {
471
- interface Window {
472
- __FA_ReplayTracker?: ReplayTrackerConstructor;
473
- __FA_getAnonymousId?: () => string;
474
- __FA_getSessionId?: () => string;
475
- __FA_sendData?: (options: SendDataOptions) => Promise<boolean>;
476
- __FA_isTrackingDisabled?: () => boolean;
477
- }
478
- }
479
-
480
- if (typeof window !== "undefined") {
481
- window.__FA_ReplayTracker = ReplayTracker;
482
- }
package/src/web-vitals.ts CHANGED
@@ -13,6 +13,7 @@ export interface WebVitalsOptions {
13
13
  endpoint?: string;
14
14
  debug?: boolean;
15
15
  samplingPercentage?: number;
16
+ getSessionId?: () => string;
16
17
  }
17
18
 
18
19
  type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
@@ -33,6 +34,7 @@ class WebVitalsTracker {
33
34
  private readonly sessionSamplingSeed: number;
34
35
  private readonly metricsMap = new Map<MetricName, MetricState>();
35
36
  private flushed = false;
37
+ private readonly getSessionId: () => string;
36
38
 
37
39
  constructor(options: WebVitalsOptions) {
38
40
  this.siteKey = options.siteKey;
@@ -44,6 +46,7 @@ class WebVitalsTracker {
44
46
  options.samplingPercentage,
45
47
  );
46
48
  this.sessionSamplingSeed = Math.random() * 100;
49
+ this.getSessionId = options.getSessionId ?? (() => "");
47
50
  }
48
51
 
49
52
  private getVitalsEndpoint(baseEndpoint: string): string {
@@ -146,7 +149,7 @@ class WebVitalsTracker {
146
149
 
147
150
  return {
148
151
  body: JSON.stringify({
149
- sessionId: window.__FA_getSessionId?.(),
152
+ sessionId: this.getSessionId(),
150
153
  vitals,
151
154
  metadata: {
152
155
  url: window.location.href,
@@ -186,15 +189,3 @@ class WebVitalsTracker {
186
189
  }
187
190
 
188
191
  export default WebVitalsTracker;
189
-
190
- declare global {
191
- interface Window {
192
- __FA_WebVitalsTracker?: typeof WebVitalsTracker;
193
- __FA_getSessionId?: () => string;
194
- __FA_isTrackingDisabled?: () => boolean;
195
- }
196
- }
197
-
198
- if (typeof window !== "undefined") {
199
- window.__FA_WebVitalsTracker = WebVitalsTracker;
200
- }