@faststats/web 0.1.0 → 0.1.2

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.2
4
+
5
+ ### Patch Changes
6
+
7
+ - 5d1cc90: fix: enforce proper minimum duration of 3 seconds
8
+
9
+ ## 0.1.1
10
+
11
+ ### Patch Changes
12
+
13
+ - 2326d00: fix: some cleanup
14
+
3
15
  ## 0.1.0
4
16
 
5
17
  ### Minor Changes
@@ -0,0 +1 @@
1
+ import{a as e,c as t,i as n,n as r,o as i,r as a,s as o,t as s}from"./types-B5zbhOWK.js";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,t(this.isCookielessMode());let e=this.options;if(e.errorTracking?.enabled??e.trackErrors){let{default:t}=await import(`./error-CMM8PYcW.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug}).start(),this.log(`error loaded`)}if(e.trackWebVitals){let{default:t}=await import(`./web-vitals-DU_i1Ejx.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,samplingPercentage:s(e.webVitals?.sampling?.percentage)}).start(),this.log(`web-vitals loaded`)}if(e.trackReplay){let{default:t}=await import(`./replay-DeRZXYG3.js`);new t({siteKey:e.siteKey,endpoint:this.endpoint,debug:this.debug,...e.replayOptions}).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(g()||this.isCookielessMode())return;let i=e.trim(),a=t.trim();!i||!a||_({url:this.endpoint.replace(/\/v1\/web$/,`/v1/identify`),data:JSON.stringify({token:this.options.siteKey,identifier:r(!1),externalId:i,email:a,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){g()||(e&&i(this.isCookielessMode()),o())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return r(this.isCookielessMode())}getSessionId(){return a()}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){let n=r(this.isCookielessMode()),i=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:a(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...y(),...t}});this.log(e),_({url:this.endpoint,data:i,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-n()})}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}e()},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{f as a,_ as c,g as i,p as l,l as n,m as o,d as r,h as s,b as t,u};
@@ -0,0 +1,3 @@
1
+ import{n as e,r as t}from"./types-B5zbhOWK.js";import{c as n}from"./analytics-p8pLmlA-.js";var r=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;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}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 r=[];for(let{entry:e,hash:t,count:n}of this.errorCounts.values())r.push({...e,hash:t,count:n});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,r);let i=e(),a=typeof document<`u`?{url:location.href,page:location.pathname,referrer:document.referrer||null}:{url:``,page:``,referrer:null},o=globalThis.__SOURCEMAPS_BUILD__,s=typeof o?.buildId==`string`&&o.buildId.trim().length>0?o.buildId:void 0,c={token:this.siteKey,...i?{userId:i}:{},sessionId:t(),...s?{buildId:s}:{},data:{url:a.url,page:a.page,referrer:a.referrer,title:typeof document<`u`?document.title:``},errors:r},l=JSON.stringify(c);this.debug&&console.log(`[ErrorTracker] Payload:`,l),n({url:this.endpoint,data:l,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{r as default};
package/dist/module.d.ts CHANGED
@@ -352,16 +352,6 @@ declare global {
352
352
  }
353
353
  }
354
354
  //#endregion
355
- //#region src/utils/types.d.ts
356
- interface SendDataOptions {
357
- url: string;
358
- data: string | Blob;
359
- contentType?: string;
360
- headers?: Record<string, string>;
361
- debug?: boolean;
362
- debugPrefix?: string;
363
- }
364
- //#endregion
365
355
  //#region src/replay.d.ts
366
356
  interface ReplayTrackerOptions {
367
357
  siteKey: string;
@@ -370,6 +360,7 @@ interface ReplayTrackerOptions {
370
360
  samplingPercentage?: number;
371
361
  flushInterval?: number;
372
362
  maxEvents?: number;
363
+ minReplayLengthMs?: number;
373
364
  sampling?: recordOptions<eventWithTime>["sampling"];
374
365
  slimDOMOptions?: SlimDOMOptions;
375
366
  maskAllInputs?: boolean;
@@ -381,9 +372,16 @@ interface ReplayTrackerOptions {
381
372
  checkoutEveryNms?: number;
382
373
  checkoutEveryNth?: number;
383
374
  recordConsole?: boolean;
384
- getAnonymousId?: () => string;
385
- getSessionId?: () => string;
386
- sendData?: (options: SendDataOptions) => Promise<boolean>;
375
+ }
376
+ //#endregion
377
+ //#region src/utils/types.d.ts
378
+ interface SendDataOptions {
379
+ url: string;
380
+ data: string | Blob;
381
+ contentType?: string;
382
+ headers?: Record<string, string>;
383
+ debug?: boolean;
384
+ debugPrefix?: string;
387
385
  }
388
386
  //#endregion
389
387
  //#region src/analytics.d.ts
@@ -404,11 +402,9 @@ interface ErrorTrackingConfig {
404
402
  enabled?: boolean;
405
403
  }
406
404
  interface WebVitalsConfig {
407
- enabled?: boolean;
408
405
  sampling?: SamplingOptions;
409
406
  }
410
407
  interface SessionReplayConfig {
411
- enabled?: boolean;
412
408
  sampling?: SamplingOptions;
413
409
  }
414
410
  type ConsentMode = "pending" | "granted" | "denied";
package/dist/module.js CHANGED
@@ -1 +1 @@
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&&!(e.tagName===`A`&&e.href);)e=e.parentNode;return e}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,`requestIdleCallback`in window?window.requestIdleCallback(()=>void this.start()):setTimeout(()=>void this.start(),1)}}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,a=r.webVitals?.sampling?.percentage!==void 0,o=r.replayOptions?.samplingPercentage!==void 0||r.sessionReplays?.sampling?.percentage!==void 0;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??r.webVitals?.enabled??a){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.sessionReplays?.enabled??r.trackReplay??o){let{default:t}=await import(`./replay-ZmimkIBT.js`),a=r.replayOptions??{};new t({siteKey:r.siteKey,endpoint:this.endpoint,debug:this.debug,...a,samplingPercentage:e(a.samplingPercentage??r.sessionReplays?.sampling?.percentage),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};
1
+ import"./types-B5zbhOWK.js";import{a as e,c as t,i as n,l as r,n as i,o as a,r as o,s,t as c,u as l}from"./analytics-p8pLmlA-.js";export{c as WebAnalytics,i as getInstance,o as identify,n as isTrackingDisabled,e as logout,a as optIn,s as optOut,t as sendData,r as setConsentMode,l as trackEvent};
@@ -0,0 +1 @@
1
+ import{n as e,r as t,t as n}from"./types-B5zbhOWK.js";import{c as r}from"./analytics-p8pLmlA-.js";import{getRecordConsolePlugin as i}from"@rrweb/rrweb-plugin-console-record";import{record as a}from"rrweb";var o=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;minLengthFlushScheduled=!1;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs;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=n(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.minReplayLengthMs=e.minReplayLengthMs??3e3,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 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=[i()]),this.stopRecording=a(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){if(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),!this.hasReachedMinLength()){this.events=[],this.debug&&console.log(`[Replay] Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush()}}handleEvent(e,t){this.events.push(e),t||this.events.length>=this.maxEvents?this.scheduleFlush():!this.minLengthFlushScheduled&&this.hasReachedMinLength()&&(this.minLengthFlushScheduled=!0,this.scheduleFlush())}hasReachedMinLength(){return this.minReplayLengthMs<=0?!0:Date.now()-this.startTime>=this.minReplayLengthMs}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}if(!this.hasReachedMinLength()){this.debug&&console.log(`[Replay] Too short (${Date.now()-this.startTime}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let n=this.events;this.events=[];let r=e(),i={token:this.siteKey,sessionId:t(),...r?{identifier:r}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:n};this.debug&&console.log(`[Replay] Sending ${n.length} events (seq: ${i.sequence})`);try{let e,t=!1;if(this.compressionSupported)try{e=await this.compress(JSON.stringify(i)),t=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),e=new Blob([JSON.stringify(i)],{type:`application/json`})}else e=new Blob([JSON.stringify(i)],{type:`application/json`});await this.send(e,t)||this.pendingBatches.push({batch:i,isCompressed:t,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.pendingBatches.push({batch:i,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,i=(e.size/1024).toFixed(1);return r({url:n,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${i}KB`})??Promise.resolve(!1)}getSessionId(){return t()}};export{o as default};
@@ -0,0 +1 @@
1
+ let e=!1;function t(t){e=t}function n(t){return t??e?``:r()}function r(){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 i(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),r())}function a(){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 o(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),a())}function s(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function c(){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()}function l(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{s as a,t as c,c as i,n,i as o,a as r,o as s,l as t};
@@ -0,0 +1 @@
1
+ import{r as e,t}from"./types-B5zbhOWK.js";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;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=t(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 t=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:e(),vitals:t,metadata:{url:window.location.href}}),count:t.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{s 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,11 +15,11 @@
12
15
  }
13
16
  },
14
17
  "type": "module",
15
- "sideEffects": false,
18
+ "sideEffects": true,
16
19
  "publishConfig": {
17
20
  "access": "public"
18
21
  },
19
- "version": "0.1.0",
22
+ "version": "0.1.2",
20
23
  "scripts": {
21
24
  "build": "tsdown",
22
25
  "dev": "bun run build",
package/src/analytics.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  refreshSessionTimestamp,
7
7
  resetAnonymousId,
8
8
  resetSessionId,
9
+ setCookielessMode,
9
10
  } from "./utils/identifiers";
10
11
  import {
11
12
  normalizeSamplingPercentage,
@@ -108,13 +109,11 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
108
109
  }
109
110
 
110
111
  function getLinkEl(el: Node | null): HTMLAnchorElement | null {
111
- while (
112
- el &&
113
- !((el as Element).tagName === "A" && (el as HTMLAnchorElement).href)
114
- ) {
112
+ while (el) {
113
+ if (el instanceof HTMLAnchorElement && el.href) return el;
115
114
  el = el.parentNode;
116
115
  }
117
- return el as HTMLAnchorElement | null;
116
+ return null;
118
117
  }
119
118
 
120
119
  function getUTM(): Record<string, string> {
@@ -145,12 +144,10 @@ interface ErrorTrackingConfig {
145
144
  }
146
145
 
147
146
  interface WebVitalsConfig {
148
- enabled?: boolean;
149
147
  sampling?: SamplingOptions;
150
148
  }
151
149
 
152
150
  interface SessionReplayConfig {
153
- enabled?: boolean;
154
151
  sampling?: SamplingOptions;
155
152
  }
156
153
 
@@ -222,9 +219,7 @@ export class WebAnalytics {
222
219
  return;
223
220
  }
224
221
  _instance = this;
225
- if ("requestIdleCallback" in window)
226
- window.requestIdleCallback(() => void this.start());
227
- else setTimeout(() => void this.start(), 1);
222
+ setTimeout(() => void this.start(), 0);
228
223
  }
229
224
 
230
225
  async start(): Promise<void> {
@@ -240,13 +235,8 @@ export class WebAnalytics {
240
235
  this.started = true;
241
236
  _instance = this;
242
237
 
243
- const getAnonymousIdFn = () => getAnonymousId(this.isCookielessMode());
238
+ setCookielessMode(this.isCookielessMode());
244
239
  const opts = this.options;
245
- const hasWebVitalsSampling =
246
- opts.webVitals?.sampling?.percentage !== undefined;
247
- const hasReplaySampling =
248
- opts.replayOptions?.samplingPercentage !== undefined ||
249
- opts.sessionReplays?.sampling?.percentage !== undefined;
250
240
 
251
241
  if (opts.errorTracking?.enabled ?? opts.trackErrors) {
252
242
  const { default: ErrorTracker } = await import("./error");
@@ -254,22 +244,10 @@ export class WebAnalytics {
254
244
  siteKey: opts.siteKey,
255
245
  endpoint: this.endpoint,
256
246
  debug: this.debug,
257
- getCommonData: () => ({
258
- url: location.href,
259
- page: location.pathname,
260
- referrer: document.referrer || null,
261
- }),
262
- getAnonymousId: getAnonymousIdFn,
263
- getSessionId: getOrCreateSessionId,
264
- sendData,
265
247
  }).start();
266
248
  this.log("error loaded");
267
249
  }
268
- if (
269
- opts.trackWebVitals ??
270
- opts.webVitals?.enabled ??
271
- hasWebVitalsSampling
272
- ) {
250
+ if (opts.trackWebVitals) {
273
251
  const { default: WebVitalsTracker } = await import("./web-vitals");
274
252
  new WebVitalsTracker({
275
253
  siteKey: opts.siteKey,
@@ -278,25 +256,16 @@ export class WebAnalytics {
278
256
  samplingPercentage: normalizeSamplingPercentage(
279
257
  opts.webVitals?.sampling?.percentage,
280
258
  ),
281
- getSessionId: getOrCreateSessionId,
282
259
  }).start();
283
260
  this.log("web-vitals loaded");
284
261
  }
285
- if (opts.sessionReplays?.enabled ?? opts.trackReplay ?? hasReplaySampling) {
262
+ if (opts.trackReplay) {
286
263
  const { default: ReplayTracker } = await import("./replay");
287
- const replayOpts = opts.replayOptions ?? {};
288
264
  new ReplayTracker({
289
265
  siteKey: opts.siteKey,
290
266
  endpoint: this.endpoint,
291
267
  debug: this.debug,
292
- ...replayOpts,
293
- samplingPercentage: normalizeSamplingPercentage(
294
- replayOpts.samplingPercentage ??
295
- opts.sessionReplays?.sampling?.percentage,
296
- ),
297
- getAnonymousId: getAnonymousIdFn,
298
- getSessionId: getOrCreateSessionId,
299
- sendData,
268
+ ...opts.replayOptions,
300
269
  }).start();
301
270
  this.log("replay loaded");
302
271
  }
@@ -375,6 +344,7 @@ export class WebAnalytics {
375
344
 
376
345
  setConsentMode(mode: ConsentMode): void {
377
346
  this.consentMode = mode;
347
+ setCookielessMode(this.isCookielessMode());
378
348
  }
379
349
 
380
350
  optIn(): void {
package/src/error.ts CHANGED
@@ -1,4 +1,5 @@
1
- import type { SendDataOptions } from "./utils/types";
1
+ import { sendData } from "./analytics";
2
+ import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
2
3
 
3
4
  export type ErrorEntry = {
4
5
  error: string;
@@ -27,10 +28,6 @@ export interface ErrorTrackingOptions {
27
28
  debug?: boolean;
28
29
  flushInterval?: number;
29
30
  maxQueueSize?: number;
30
- getCommonData?: () => Record<string, unknown>;
31
- getAnonymousId?: () => string;
32
- getSessionId?: () => string;
33
- sendData?: (options: SendDataOptions) => Promise<boolean>;
34
31
  }
35
32
 
36
33
  class ErrorTracker {
@@ -39,10 +36,6 @@ class ErrorTracker {
39
36
  private readonly debug: boolean;
40
37
  private readonly flushInterval: number;
41
38
  private readonly maxQueueSize: number;
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>;
46
39
 
47
40
  private readonly handledErrors = new WeakSet<Error>();
48
41
  private readonly errorCounts = new Map<
@@ -58,10 +51,6 @@ class ErrorTracker {
58
51
  this.debug = options.debug ?? false;
59
52
  this.flushInterval = options.flushInterval ?? 5000;
60
53
  this.maxQueueSize = options.maxQueueSize ?? 50;
61
- this.getCommonData = options.getCommonData ?? (() => ({}));
62
- this.getAnonymousId = options.getAnonymousId ?? (() => "");
63
- this.getSessionId = options.getSessionId ?? (() => "");
64
- this.sendData = options.sendData ?? (async () => false);
65
54
  }
66
55
 
67
56
  start(): void {
@@ -268,8 +257,15 @@ class ErrorTracker {
268
257
  console.log("[ErrorTracker] Flushing errors:", errors);
269
258
  }
270
259
 
271
- const commonData = this.getCommonData();
272
- const identifier = this.getAnonymousId();
260
+ const identifier = getAnonymousId();
261
+ const commonData =
262
+ typeof document !== "undefined"
263
+ ? {
264
+ url: location.href,
265
+ page: location.pathname,
266
+ referrer: document.referrer || null,
267
+ }
268
+ : { url: "", page: "", referrer: null };
273
269
  const sourcemapsBuild = (
274
270
  globalThis as {
275
271
  __SOURCEMAPS_BUILD__?: { buildId?: unknown };
@@ -284,7 +280,7 @@ class ErrorTracker {
284
280
  const payload: Record<string, unknown> = {
285
281
  token: this.siteKey,
286
282
  ...(identifier ? { userId: identifier } : {}),
287
- sessionId: this.getSessionId(),
283
+ sessionId: getOrCreateSessionId(),
288
284
  ...(buildId ? { buildId } : {}),
289
285
  data: {
290
286
  url: commonData.url as string,
@@ -301,7 +297,7 @@ class ErrorTracker {
301
297
  console.log("[ErrorTracker] Payload:", payloadString);
302
298
  }
303
299
 
304
- this.sendData({
300
+ sendData({
305
301
  url: this.endpoint,
306
302
  data: payloadString,
307
303
  debug: this.debug,
package/src/replay.ts CHANGED
@@ -3,10 +3,9 @@ import type { eventWithTime, listenerHandler } from "@rrweb/types";
3
3
  import { record } from "rrweb";
4
4
  import type { recordOptions } from "rrweb/typings/types";
5
5
  import type { SlimDOMOptions } from "rrweb-snapshot";
6
- import {
7
- normalizeSamplingPercentage,
8
- type SendDataOptions,
9
- } from "./utils/types";
6
+ import { sendData } from "./analytics";
7
+ import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
8
+ import { normalizeSamplingPercentage } from "./utils/types";
10
9
 
11
10
  export interface ReplayTrackerOptions {
12
11
  siteKey: string;
@@ -15,6 +14,7 @@ export interface ReplayTrackerOptions {
15
14
  samplingPercentage?: number;
16
15
  flushInterval?: number;
17
16
  maxEvents?: number;
17
+ minReplayLengthMs?: number;
18
18
  sampling?: recordOptions<eventWithTime>["sampling"];
19
19
  slimDOMOptions?: SlimDOMOptions;
20
20
  maskAllInputs?: boolean;
@@ -26,9 +26,6 @@ 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>;
32
29
  }
33
30
 
34
31
  interface PendingBatch {
@@ -68,15 +65,13 @@ class ReplayTracker {
68
65
  private stopRecording: listenerHandler | undefined = undefined;
69
66
  private started = false;
70
67
  private startTime: number = 0;
68
+ private minLengthFlushScheduled = false;
71
69
  private sequenceNumber = 0;
72
70
  private pendingBatches: PendingBatch[] = [];
73
71
  private isFlushing = false;
74
72
  private compressionSupported = false;
75
73
  private readonly sessionSamplingSeed: number;
76
- private readonly minReplayLengthMs = 3000;
77
- private readonly _getAnonymousId: () => string;
78
- private readonly _getSessionId: () => string;
79
- private readonly sendData: (options: SendDataOptions) => Promise<boolean>;
74
+ private readonly minReplayLengthMs: number;
80
75
 
81
76
  constructor(options: ReplayTrackerOptions) {
82
77
  this.siteKey = options.siteKey;
@@ -120,10 +115,8 @@ class ReplayTracker {
120
115
  this.checkoutEveryNms = options.checkoutEveryNms ?? 60_000;
121
116
  this.checkoutEveryNth = options.checkoutEveryNth;
122
117
  this.recordConsole = options.recordConsole ?? true;
118
+ this.minReplayLengthMs = options.minReplayLengthMs ?? 3000;
123
119
  this.sessionSamplingSeed = Math.random() * 100;
124
- this._getAnonymousId = options.getAnonymousId ?? (() => "");
125
- this._getSessionId = options.getSessionId ?? (() => "");
126
- this.sendData = options.sendData ?? (async () => false);
127
120
 
128
121
  if (typeof window !== "undefined") {
129
122
  this.compressionSupported = "CompressionStream" in window;
@@ -210,12 +203,11 @@ class ReplayTracker {
210
203
  this.handleVisibilityChange,
211
204
  );
212
205
 
213
- const elapsedTime = Date.now() - this.startTime;
214
- if (elapsedTime < this.minReplayLengthMs) {
206
+ if (!this.hasReachedMinLength()) {
215
207
  this.events = [];
216
208
  if (this.debug) {
217
209
  console.log(
218
- `[Replay] Session too short (${elapsedTime}ms), discarding events`,
210
+ `[Replay] Session too short (${Date.now() - this.startTime}ms), discarding events`,
219
211
  );
220
212
  }
221
213
  return;
@@ -231,9 +223,17 @@ class ReplayTracker {
231
223
  this.scheduleFlush();
232
224
  } else if (this.events.length >= this.maxEvents) {
233
225
  this.scheduleFlush();
226
+ } else if (!this.minLengthFlushScheduled && this.hasReachedMinLength()) {
227
+ this.minLengthFlushScheduled = true;
228
+ this.scheduleFlush();
234
229
  }
235
230
  }
236
231
 
232
+ private hasReachedMinLength(): boolean {
233
+ if (this.minReplayLengthMs <= 0) return true;
234
+ return Date.now() - this.startTime >= this.minReplayLengthMs;
235
+ }
236
+
237
237
  private scheduleFlush(): void {
238
238
  if (this.isFlushing || this.events.length === 0) return;
239
239
 
@@ -270,10 +270,11 @@ class ReplayTracker {
270
270
  return;
271
271
  }
272
272
 
273
- const elapsedTime = Date.now() - this.startTime;
274
- if (elapsedTime < this.minReplayLengthMs) {
273
+ if (!this.hasReachedMinLength()) {
275
274
  if (this.debug) {
276
- console.log(`[Replay] Too short (${elapsedTime}ms), skipping`);
275
+ console.log(
276
+ `[Replay] Too short (${Date.now() - this.startTime}ms), skipping`,
277
+ );
277
278
  }
278
279
  this.processPendingBatches();
279
280
  return;
@@ -285,10 +286,10 @@ class ReplayTracker {
285
286
  const eventsToSend = this.events;
286
287
  this.events = [];
287
288
 
288
- const anonymousId = this._getAnonymousId();
289
+ const anonymousId = getAnonymousId();
289
290
  const batch = {
290
291
  token: this.siteKey,
291
- sessionId: this._getSessionId(),
292
+ sessionId: getOrCreateSessionId(),
292
293
  ...(anonymousId ? { identifier: anonymousId } : {}),
293
294
  sequence: this.sequenceNumber++,
294
295
  timestamp: Date.now(),
@@ -446,7 +447,7 @@ class ReplayTracker {
446
447
  const sizeKB = (data.size / 1024).toFixed(1);
447
448
 
448
449
  return (
449
- this.sendData({
450
+ sendData({
450
451
  url,
451
452
  data,
452
453
  contentType: isCompressed
@@ -460,7 +461,7 @@ class ReplayTracker {
460
461
  }
461
462
 
462
463
  getSessionId(): string | undefined {
463
- return this._getSessionId();
464
+ return getOrCreateSessionId();
464
465
  }
465
466
  }
466
467
 
@@ -1,7 +1,14 @@
1
1
  const SESSION_TIMEOUT = 30 * 60 * 1000;
2
2
 
3
+ let cookielessMode = false;
4
+
5
+ export function setCookielessMode(mode: boolean): void {
6
+ cookielessMode = mode;
7
+ }
8
+
3
9
  export function getAnonymousId(cookieless?: boolean): string {
4
- if (cookieless) return "";
10
+ const useCookieless = cookieless ?? cookielessMode;
11
+ if (useCookieless) return "";
5
12
  return getOrCreateAnonymousId();
6
13
  }
7
14
 
package/src/web-vitals.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  onLCP,
7
7
  onTTFB,
8
8
  } from "web-vitals/attribution";
9
+ import { getOrCreateSessionId } from "./utils/identifiers";
9
10
  import { normalizeSamplingPercentage } from "./utils/types";
10
11
 
11
12
  export interface WebVitalsOptions {
@@ -13,7 +14,6 @@ export interface WebVitalsOptions {
13
14
  endpoint?: string;
14
15
  debug?: boolean;
15
16
  samplingPercentage?: number;
16
- getSessionId?: () => string;
17
17
  }
18
18
 
19
19
  type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
@@ -34,7 +34,6 @@ class WebVitalsTracker {
34
34
  private readonly sessionSamplingSeed: number;
35
35
  private readonly metricsMap = new Map<MetricName, MetricState>();
36
36
  private flushed = false;
37
- private readonly getSessionId: () => string;
38
37
 
39
38
  constructor(options: WebVitalsOptions) {
40
39
  this.siteKey = options.siteKey;
@@ -46,7 +45,6 @@ class WebVitalsTracker {
46
45
  options.samplingPercentage,
47
46
  );
48
47
  this.sessionSamplingSeed = Math.random() * 100;
49
- this.getSessionId = options.getSessionId ?? (() => "");
50
48
  }
51
49
 
52
50
  private getVitalsEndpoint(baseEndpoint: string): string {
@@ -149,7 +147,7 @@ class WebVitalsTracker {
149
147
 
150
148
  return {
151
149
  body: JSON.stringify({
152
- sessionId: this.getSessionId(),
150
+ sessionId: getOrCreateSessionId(),
153
151
  vitals,
154
152
  metadata: {
155
153
  url: window.location.href,
@@ -1,3 +0,0 @@
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};
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
1
- function e(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{e as t};
@@ -1 +0,0 @@
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};