@faststats/web 0.1.5 → 0.1.7

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.7
4
+
5
+ ### Patch Changes
6
+
7
+ - 7d6d563: fix: better sending
8
+
9
+ ## 0.1.6
10
+
11
+ ### Patch Changes
12
+
13
+ - 48a1008: refactor: perf & bulletproof parts of the core sdk
14
+
3
15
  ## 0.1.5
4
16
 
5
17
  ### Patch Changes
@@ -0,0 +1 @@
1
+ import{a as e,c as t,f as n,i as r,l as i,n as a,o,r as s,s as c,t as l,u}from"./types-8FXsUqbi.js";let d=null,f;function p(){return d}function m(e,t){typeof window>`u`||x()||d?.track(e,t??{})}function h(e,t,n){typeof window>`u`||x()||d?.identify(e,t,n??{})}function g(e=!0){typeof window>`u`||x()||d?.logout(e)}function _(e){if(d){d.setConsentMode(e);return}f=e}function v(){_(`granted`)}function y(){_(`denied`)}function b(e){typeof window>`u`||x()||d?.reportError(e)}function x(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function S(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0}=e;if(s&&typeof navigator<`u`&&typeof navigator.sendBeacon==`function`)try{let e=n instanceof Blob?n:typeof Blob<`u`?new Blob([n],{type:r}):n;if(navigator.sendBeacon(t,e))return a&&console.log(`${o} Sent via beacon`),!0}catch{}if(typeof fetch!=`function`)return a&&console.warn(`${o} Failed to send`),!1;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 C(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function w(){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 T=class{webEndpoint;baseUrl;debug;started=!1;destroyed=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;cleanupCallbacks=[];childTrackers=[];pendingReportedErrors=[];errorTracker=null;handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.leavePage(),this.stopHeartbeat()):this.startHeartbeat()};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(e){this.options=e,this.baseUrl=u(e.baseUrl),this.webEndpoint=n(this.baseUrl),this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,f!==void 0&&(this.consentMode=f,f=void 0),(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(x()){this.log(`disabled`);return}d=this,t(this.isCookielessMode()),setTimeout(()=>void this.start(),0)}}registerCleanup(e){this.cleanupCallbacks.push(e)}addWindowListener(e,t){window.addEventListener(e,t),this.registerCleanup(()=>window.removeEventListener(e,t))}addDocumentListener(e,t){document.addEventListener(e,t),this.registerCleanup(()=>document.removeEventListener(e,t))}patchHistory(){let e=history.pushState.bind(history),t=history.replaceState.bind(history);history.pushState=(t,n,r)=>{e(t,n,r),this.navigate()},history.replaceState=(e,n,r)=>{t(e,n,r),this.navigate()},this.registerCleanup(()=>{history.pushState=e,history.replaceState=t})}ensureStarted(){return typeof window>`u`||this.destroyed||x()?!1:(this.started||this.start(),!0)}stopHeartbeat(){this.heartbeatTimer&&=(clearInterval(this.heartbeatTimer),null)}stopNavigationTimer(){this.navTimer&&=(clearTimeout(this.navTimer),null)}stopChildTrackers(){for(let e of this.childTrackers.splice(0))e.stop?.();this.errorTracker=null}getErrorTracker(){return this.errorTracker}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(d&&d!==this){this.log(`already started by another instance`);return}if(x()){this.log(`disabled`);return}this.started=!0,d=this,t(this.isCookielessMode());let e=this.options;if(e.trackErrors)try{let{default:t}=await import(`./error-62TEqOxN.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug});for(n.start(),this.errorTracker=n,this.childTrackers.push(n);this.pendingReportedErrors.length>0;){let e=this.pendingReportedErrors.shift();e&&n.captureError(e)}this.log(`error loaded`)}catch(e){this.log(`failed to initialize error tracker: ${String(e)}`)}if(e.trackWebVitals)try{let{default:t}=await import(`./web-vitals-C7H69Xv3.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:l(e.webVitals?.sampling?.percentage)});n.start(),this.childTrackers.push(n),this.log(`web-vitals loaded`)}catch(e){this.log(`failed to initialize web-vitals tracker: ${String(e)}`)}if(e.trackReplay)try{let{default:t}=await import(`./replay-SOvAbLbE.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,...e.replayOptions});n.start(),this.childTrackers.push(n),this.log(`replay loaded`)}catch(e){this.log(`failed to initialize replay tracker: ${String(e)}`)}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),this.addDocumentListener(`visibilitychange`,this.handleVisibilityChange),this.addWindowListener(`pagehide`,this.handlePageHide),this.addWindowListener(`popstate`,this.handlePopState),e.trackHash&&this.addWindowListener(`hashchange`,this.handleHashChange),this.patchHistory()}destroy(){if(!this.destroyed){for(this.started&&typeof window<`u`&&this.leavePage(),this.pendingReportedErrors.length=0,this.stopNavigationTimer(),this.stopHeartbeat(),this.scrollHandler&&typeof window<`u`&&(window.removeEventListener(`scroll`,this.scrollHandler),this.scrollHandler=null);this.cleanupCallbacks.length>0;)this.cleanupCallbacks.pop()?.();this.stopChildTrackers(),d===this&&(d=null),this.started=!1,this.destroyed=!0}}pageview(e={}){if(!this.ensureStarted())return;let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.ensureStarted()&&this.send(e,t)}identify(e,t,n={}){if(!this.ensureStarted()||this.isCookielessMode())return;let r=e.trim(),o=t.trim();!r||!o||S({url:i(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:a(!1),externalId:r,email:o,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){this.ensureStarted()&&(e&&o(this.isCookielessMode()),c())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return a(this.isCookielessMode())}getSessionId(){return s()}reportError(e){if(this.destroyed||typeof window>`u`||x()||!(this.options.trackErrors??!1)||!this.ensureStarted())return;let t=this.getErrorTracker();if(t){t.captureError(e);return}this.pendingReportedErrors.length>=50&&this.pendingReportedErrors.shift(),this.pendingReportedErrors.push(e)}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){if(typeof window>`u`||this.destroyed||x())return;let n=a(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:s(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...w(),...t}});this.log(e),S({url:this.webEndpoint,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.destroyed||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-r()})}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.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){this.stopHeartbeat();return}e()},300*1e3)}navigate(){!this.started||this.destroyed||(this.stopNavigationTimer(),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))}links(){let e=e=>{let t=C(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{g as a,b as c,m as d,x as i,S as l,p as n,v as o,h as r,y as s,T as t,_ as u};
@@ -0,0 +1,2 @@
1
+ import{f as e,n as t,r as n}from"./types-8FXsUqbi.js";import{l as r}from"./analytics-CI4BwqHE.js";const i=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;function a(e){if(e)return e.split(`
2
+ `).map(e=>e.trim()).filter(e=>e.length>0)}function o(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:a(e.stack),cause:o(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function s(e){return e?`${e.error}\0${e.message??``}\0${s(e.cause)}`:``}function c(e){let t=[e.type,e.handled?`handled`:`unhandled`,e.message,e.filename??``,e.lineno??``,s(e.cause)].join(`\0`),n=2166136261,r=3598710387;for(let e=0;e<t.length;e++){let i=t.charCodeAt(e);n=Math.imul(n^i,16777619),r=Math.imul(r^i,2246822519)}return`err_${(n>>>0).toString(16).padStart(8,`0`)}${(r>>>0).toString(16).padStart(8,`0`)}`}var l=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;handledErrors=new WeakSet;errorCounts=new Map;handleVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush()};handlePageHide=()=>{this.flush()};flushTimer=null;started=!1;constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.flushInterval=t.flushInterval??5e3,this.maxQueueSize=t.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`,this.handleVisibilityChange),window.addEventListener(`pagehide`,this.handlePageHide),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),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),window.removeEventListener(`pagehide`,this.handlePageHide),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}skipDuplicate(e){return e instanceof Error?this.handledErrors.has(e)?!0:(this.handledErrors.add(e),!1):!1}handleErrorEvent=e=>{let t=e.error;if(this.skipDuplicate(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.queueError({message:e.message||(t instanceof Error?t.message:`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t instanceof Error?t.stack:void 0,type:`error`,handled:!1,cause:t instanceof Error?o(t.cause):void 0})};handleRejection=e=>{let t=e.reason;if(this.skipDuplicate(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t instanceof Error?t.message:t);return}let n=t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`;this.queueError({message:n,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`,handled:!1,cause:t instanceof Error?o(t.cause):void 0})};queueError(e){if(i.test(`${e.filename??``}\n${e.stack??``}`))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=c(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:a(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}};this.errorCounts.set(t,{entry:n,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.skipDuplicate(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate:`,e.message);return}this.queueError({message:e.message,stack:e.stack,type:`error`,handled:!0,cause:o(e.cause)})}flush(){if(this.errorCounts.size===0)return;let e=[];for(let[t,{entry:n,count:r}]of this.errorCounts)e.push({...n,hash:t,count:r});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,e);let i=t(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim().length>0?a.buildId:void 0,s=JSON.stringify({token:this.siteKey,...i?{userId:i}:{},sessionId:n(),...o?{buildId:o}:{},data:{url:location.href,page:location.pathname,referrer:document.referrer||null,title:document.title},errors:e});this.debug&&console.log(`[ErrorTracker] Payload:`,s),r({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{l as default};
package/dist/module.d.ts CHANGED
@@ -360,6 +360,7 @@ interface ReplayTrackerOptions {
360
360
  samplingPercentage?: number;
361
361
  flushInterval?: number;
362
362
  maxEvents?: number;
363
+ maxPendingBatches?: number;
363
364
  minReplayLengthMs?: number;
364
365
  sampling?: recordOptions<eventWithTime>["sampling"];
365
366
  slimDOMOptions?: SlimDOMOptions;
@@ -382,6 +383,7 @@ interface SendDataOptions {
382
383
  headers?: Record<string, string>;
383
384
  debug?: boolean;
384
385
  debugPrefix?: string;
386
+ useBeacon?: boolean;
385
387
  }
386
388
  //#endregion
387
389
  //#region src/analytics.d.ts
@@ -453,6 +455,7 @@ declare class WebAnalytics {
453
455
  private readonly cleanupCallbacks;
454
456
  private readonly childTrackers;
455
457
  private readonly pendingReportedErrors;
458
+ private errorTracker;
456
459
  private readonly handleVisibilityChange;
457
460
  private readonly handlePageHide;
458
461
  private readonly handlePopState;
package/dist/module.js CHANGED
@@ -1 +1 @@
1
- import"./types-8FXsUqbi.js";import{a as e,c as t,d as n,i as r,l as i,n as a,o,r as s,s as c,t as l,u}from"./analytics-DO2MFOy_.js";export{l as WebAnalytics,a as getInstance,s as identify,r as isTrackingDisabled,e as logout,o as optIn,c as optOut,t as reportError,i as sendData,u as setConsentMode,n as trackEvent};
1
+ import"./types-8FXsUqbi.js";import{a as e,c as t,d as n,i as r,l as i,n as a,o,r as s,s as c,t as l,u}from"./analytics-CI4BwqHE.js";export{l as WebAnalytics,a as getInstance,s as identify,r as isTrackingDisabled,e as logout,o as optIn,c as optOut,t as reportError,i as sendData,u as setConsentMode,n as trackEvent};
@@ -0,0 +1 @@
1
+ import{d as e,n as t,r as n,t as r}from"./types-8FXsUqbi.js";import{l as i}from"./analytics-CI4BwqHE.js";import{getRecordConsolePlugin as a}from"@rrweb/rrweb-plugin-console-record";import{getRecordSequentialIdPlugin as o}from"@rrweb/rrweb-plugin-sequential-id-record";import{record as s}from"rrweb";var c=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;maxPendingBatches;flushScheduled=!1;flushTimeout=null;idleFlushId=null;pendingRetryTimer=null;isProcessingPending=!1;constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.samplingPercentage=r(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.minReplayLengthMs=t.minReplayLengthMs??3e3,this.maxPendingBatches=t.maxPendingBatches??30,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),e.plugins=[o({key:`_faststatsSeqId`}),...this.recordConsole?[a()]:[]],this.stopRecording=s(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.clearScheduledFlush(),this.clearPendingRetryTimer(),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(){if(this.isFlushing||this.events.length===0||this.flushScheduled)return;this.flushScheduled=!0;let e=()=>{this.flushScheduled=!1,this.flushTimeout=null,this.idleFlushId=null,this.flush()};if(typeof window<`u`&&`requestIdleCallback`in window){this.idleFlushId=window.requestIdleCallback(e,{timeout:2e3});return}this.flushTimeout=setTimeout(e,0)}handleUnload=()=>{this.clearScheduledFlush(),this.flush({lowLatency:!0})};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.clearScheduledFlush(),this.flush({lowLatency:!0})):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};clearScheduledFlush(){this.idleFlushId!==null&&typeof window<`u`&&`cancelIdleCallback`in window&&window.cancelIdleCallback(this.idleFlushId),this.idleFlushId=null,this.flushTimeout&&=(clearTimeout(this.flushTimeout),null),this.flushScheduled=!1}clearPendingRetryTimer(){this.pendingRetryTimer&&=(clearTimeout(this.pendingRetryTimer),null)}queuePendingBatch(e){this.pendingBatches.length>=this.maxPendingBatches&&this.pendingBatches.shift(),this.pendingBatches.push(e)}schedulePendingRetry(e){this.pendingRetryTimer||=setTimeout(()=>{this.pendingRetryTimer=null,this.processPendingBatches()},e)}async flush(e={}){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 r=this.events;this.events=[];let i=t(),a={token:this.siteKey,sessionId:n(),...i?{identifier:i}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:r};if(this.pendingBatches.length>0||this.isProcessingPending){this.queuePendingBatch({batch:a,isCompressed:this.compressionSupported&&!e.lowLatency,retries:0}),this.isFlushing=!1,this.processPendingBatches();return}this.debug&&console.log(`[Replay] Sending ${r.length} events (seq: ${a.sequence})`);try{let t,n=!1;if(this.compressionSupported&&!e.lowLatency)try{t=await this.compress(JSON.stringify(a)),n=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),t=new Blob([JSON.stringify(a)],{type:`application/json`})}else t=new Blob([JSON.stringify(a)],{type:`application/json`});await this.send(t,n,{useBeacon:e.lowLatency===!0})||this.queuePendingBatch({batch:a,isCompressed:n,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.queuePendingBatch({batch:a,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(!(this.isProcessingPending||this.pendingBatches.length===0)){this.clearPendingRetryTimer(),this.isProcessingPending=!0;try{for(;this.pendingBatches.length>0;){let e=this.pendingBatches[0];if(!e)break;if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, restoring batch ${e.batch.sequence} to buffer`),this.pendingBatches.shift(),this.events=[...e.batch.events,...this.events],this.scheduleFlush();continue}e.retries++;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`});if(await this.send(t,e.isCompressed,{useBeacon:!1})){this.pendingBatches.shift();continue}this.schedulePendingRetry(1e3*e.retries);break}}catch{let e=this.pendingBatches[0];this.debug&&e&&console.warn(`[Replay] Retry ${e.retries} failed`),this.schedulePendingRetry(e?1e3*e.retries:1e3)}finally{this.isProcessingPending=!1}}}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,n={}){let r=t?`${this.endpoint}?encoding=gzip`:this.endpoint,a=(e.size/1024).toFixed(1);return i({url:r,data:e,contentType:t?`application/octet-stream`:`application/json`,headers:t?{"Content-Encoding":`gzip`}:void 0,debug:this.debug,debugPrefix:`[Replay] ${a}KB`,useBeacon:n.useBeacon??!1})??Promise.resolve(!1)}getSessionId(){return n()}};export{c as default};
@@ -1 +1 @@
1
- import{p as e,r as t,t as n}from"./types-8FXsUqbi.js";import{onCLS as r,onFCP as i,onINP as a,onLCP as o,onTTFB as s}from"web-vitals/attribution";var c=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;handleVisibilityChange=()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()};handlePageHide=()=>{this.finalizeAndFlush()};constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.samplingPercentage=n(t.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}start(){this.started||typeof window>`u`||(this.started=!0,this.flushed=!1,document.addEventListener(`visibilitychange`,this.handleVisibilityChange),window.addEventListener(`pagehide`,this.handlePageHide),this.debug&&console.log(`[WebVitals] Tracking started`),r(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),o(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),s(e=>this.captureMetric(e)))}stop(){!this.started||typeof window>`u`||(this.started=!1,document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),window.removeEventListener(`pagehide`,this.handlePageHide),this.finalizeAndFlush())}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:t(),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{c as default};
1
+ import{p as e,r as t,t as n}from"./types-8FXsUqbi.js";import{onCLS as r,onFCP as i,onINP as a,onLCP as o,onTTFB as s}from"web-vitals/attribution";var c=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;handleVisibilityChange=()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()};handlePageHide=()=>{this.finalizeAndFlush()};constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.samplingPercentage=n(t.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}start(){this.started||typeof window>`u`||(this.started=!0,this.flushed=!1,document.addEventListener(`visibilitychange`,this.handleVisibilityChange),window.addEventListener(`pagehide`,this.handlePageHide),this.debug&&console.log(`[WebVitals] Tracking started`),r(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),o(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),s(e=>this.captureMetric(e)))}stop(){!this.started||typeof window>`u`||(this.started=!1,document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),window.removeEventListener(`pagehide`,this.handlePageHide),this.finalizeAndFlush())}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:t(),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}`)}typeof fetch==`function`&&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{c as default};
package/package.json CHANGED
@@ -19,12 +19,14 @@
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
22
- "version": "0.1.5",
22
+ "version": "0.1.7",
23
23
  "scripts": {
24
24
  "build": "tsdown",
25
25
  "dev": "bun run build",
26
26
  "typecheck": "tsc --noEmit -p tsconfig.json",
27
27
  "test": "bun test",
28
+ "test:perf": "bun test tests/replay-stress.test.ts",
29
+ "check:bundle-size": "node scripts/check-bundle-size.mjs",
28
30
  "deploy:worker": "wrangler deploy"
29
31
  },
30
32
  "devDependencies": {
@@ -0,0 +1,15 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+
4
+ const MAX_BYTES = 60 * 1024;
5
+ const bundlePath = resolve(process.cwd(), "dist/module.js");
6
+ const file = await stat(bundlePath);
7
+
8
+ if (file.size > MAX_BYTES) {
9
+ console.error(
10
+ `Bundle size check failed: ${file.size} bytes exceeds ${MAX_BYTES} bytes.`,
11
+ );
12
+ process.exit(1);
13
+ }
14
+
15
+ console.log(`Bundle size check passed: ${file.size} bytes.`);
package/src/analytics.ts CHANGED
@@ -73,6 +73,7 @@ export interface WebAnalyticsOptions {
73
73
  }
74
74
 
75
75
  let _instance: WebAnalytics | null = null;
76
+ let pendingConsentMode: ConsentMode | undefined;
76
77
 
77
78
  export function getInstance(): WebAnalytics | null {
78
79
  return _instance;
@@ -101,7 +102,11 @@ export function logout(resetAnonymousIdentity = true): void {
101
102
  }
102
103
 
103
104
  export function setConsentMode(mode: ConsentMode): void {
104
- _instance?.setConsentMode(mode);
105
+ if (_instance) {
106
+ _instance.setConsentMode(mode);
107
+ return;
108
+ }
109
+ pendingConsentMode = mode;
105
110
  }
106
111
 
107
112
  export function optIn(): void {
@@ -131,16 +136,35 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
131
136
  headers = {},
132
137
  debug = false,
133
138
  debugPrefix = "[Analytics]",
139
+ useBeacon = true,
134
140
  } = options;
135
141
 
136
- const blob =
137
- data instanceof Blob ? data : new Blob([data], { type: contentType });
142
+ const canUseBeacon =
143
+ useBeacon &&
144
+ typeof navigator !== "undefined" &&
145
+ typeof navigator.sendBeacon === "function";
146
+ if (canUseBeacon) {
147
+ try {
148
+ const beaconBody =
149
+ data instanceof Blob
150
+ ? data
151
+ : typeof Blob !== "undefined"
152
+ ? new Blob([data], { type: contentType })
153
+ : data;
154
+ if (navigator.sendBeacon(url, beaconBody)) {
155
+ if (debug) {
156
+ console.log(`${debugPrefix} Sent via beacon`);
157
+ }
158
+ return true;
159
+ }
160
+ } catch {}
161
+ }
138
162
 
139
- if (navigator.sendBeacon?.(url, blob)) {
163
+ if (typeof fetch !== "function") {
140
164
  if (debug) {
141
- console.log(`${debugPrefix} Sent via beacon`);
165
+ console.warn(`${debugPrefix} Failed to send`);
142
166
  }
143
- return true;
167
+ return false;
144
168
  }
145
169
 
146
170
  try {
@@ -216,6 +240,7 @@ export class WebAnalytics {
216
240
  private readonly cleanupCallbacks: Array<() => void> = [];
217
241
  private readonly childTrackers: ChildTracker[] = [];
218
242
  private readonly pendingReportedErrors: Error[] = [];
243
+ private errorTracker: ErrorTracker | null = null;
219
244
  private readonly handleVisibilityChange = (): void => {
220
245
  if (document.visibilityState === "hidden") {
221
246
  this.leavePage();
@@ -241,6 +266,10 @@ export class WebAnalytics {
241
266
  this.consentMode = options.consent?.mode ?? "granted";
242
267
  this.cookielessWhilePending =
243
268
  options.consent?.cookielessWhilePending ?? true;
269
+ if (pendingConsentMode !== undefined) {
270
+ this.consentMode = pendingConsentMode;
271
+ pendingConsentMode = undefined;
272
+ }
244
273
  if (options.autoTrack ?? true) this.init();
245
274
  }
246
275
 
@@ -330,13 +359,11 @@ export class WebAnalytics {
330
359
  for (const tracker of this.childTrackers.splice(0)) {
331
360
  tracker.stop?.();
332
361
  }
362
+ this.errorTracker = null;
333
363
  }
334
364
 
335
365
  private getErrorTracker(): ErrorTracker | null {
336
- for (const t of this.childTrackers) {
337
- if ("captureError" in t) return t as ErrorTracker;
338
- }
339
- return null;
366
+ return this.errorTracker;
340
367
  }
341
368
 
342
369
  async start(): Promise<void> {
@@ -355,8 +382,8 @@ export class WebAnalytics {
355
382
  setCookielessMode(this.isCookielessMode());
356
383
 
357
384
  const opts = this.options;
358
- try {
359
- if (opts.trackErrors) {
385
+ if (opts.trackErrors) {
386
+ try {
360
387
  const { default: ErrorTrackerClass } = await import("./error");
361
388
  const errorTracker: ErrorTracker = new ErrorTrackerClass({
362
389
  siteKey: opts.siteKey,
@@ -364,14 +391,19 @@ export class WebAnalytics {
364
391
  debug: this.debug,
365
392
  });
366
393
  errorTracker.start();
394
+ this.errorTracker = errorTracker;
367
395
  this.childTrackers.push(errorTracker);
368
396
  while (this.pendingReportedErrors.length > 0) {
369
397
  const pending = this.pendingReportedErrors.shift();
370
398
  if (pending) errorTracker.captureError(pending);
371
399
  }
372
400
  this.log("error loaded");
401
+ } catch (error) {
402
+ this.log(`failed to initialize error tracker: ${String(error)}`);
373
403
  }
374
- if (opts.trackWebVitals) {
404
+ }
405
+ if (opts.trackWebVitals) {
406
+ try {
375
407
  const { default: WebVitalsTrackerClass } = await import("./web-vitals");
376
408
  const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
377
409
  siteKey: opts.siteKey,
@@ -384,8 +416,12 @@ export class WebAnalytics {
384
416
  webVitalsTracker.start();
385
417
  this.childTrackers.push(webVitalsTracker);
386
418
  this.log("web-vitals loaded");
419
+ } catch (error) {
420
+ this.log(`failed to initialize web-vitals tracker: ${String(error)}`);
387
421
  }
388
- if (opts.trackReplay) {
422
+ }
423
+ if (opts.trackReplay) {
424
+ try {
389
425
  const { default: ReplayTrackerClass } = await import("./replay");
390
426
  const replayTracker: ReplayTracker = new ReplayTrackerClass({
391
427
  siteKey: opts.siteKey,
@@ -396,11 +432,9 @@ export class WebAnalytics {
396
432
  replayTracker.start();
397
433
  this.childTrackers.push(replayTracker);
398
434
  this.log("replay loaded");
435
+ } catch (error) {
436
+ this.log(`failed to initialize replay tracker: ${String(error)}`);
399
437
  }
400
- } catch (error) {
401
- this.log(`failed to initialize trackers: ${String(error)}`);
402
- this.destroy();
403
- return;
404
438
  }
405
439
 
406
440
  this.enterPage();
package/src/error.ts CHANGED
@@ -98,6 +98,12 @@ class ErrorTracker {
98
98
  string,
99
99
  { entry: ErrorEntry; count: number }
100
100
  >();
101
+ private readonly handleVisibilityChange = (): void => {
102
+ if (document.visibilityState === "hidden") this.flush();
103
+ };
104
+ private readonly handlePageHide = (): void => {
105
+ this.flush();
106
+ };
101
107
  private flushTimer: ReturnType<typeof setInterval> | null = null;
102
108
  private started = false;
103
109
 
@@ -117,10 +123,8 @@ class ErrorTracker {
117
123
  window.addEventListener("unhandledrejection", this.handleRejection);
118
124
 
119
125
  this.flushTimer = setInterval(() => this.flush(), this.flushInterval);
120
- document.addEventListener("visibilitychange", () => {
121
- if (document.visibilityState === "hidden") this.flush();
122
- });
123
- window.addEventListener("pagehide", () => this.flush());
126
+ document.addEventListener("visibilitychange", this.handleVisibilityChange);
127
+ window.addEventListener("pagehide", this.handlePageHide);
124
128
 
125
129
  if (this.debug) {
126
130
  console.log("[ErrorTracker] Started listening for errors");
@@ -133,6 +137,11 @@ class ErrorTracker {
133
137
 
134
138
  window.removeEventListener("error", this.handleErrorEvent);
135
139
  window.removeEventListener("unhandledrejection", this.handleRejection);
140
+ document.removeEventListener(
141
+ "visibilitychange",
142
+ this.handleVisibilityChange,
143
+ );
144
+ window.removeEventListener("pagehide", this.handlePageHide);
136
145
 
137
146
  if (this.flushTimer) {
138
147
  clearInterval(this.flushTimer);
package/src/replay.ts CHANGED
@@ -18,6 +18,7 @@ export interface ReplayTrackerOptions {
18
18
  samplingPercentage?: number;
19
19
  flushInterval?: number;
20
20
  maxEvents?: number;
21
+ maxPendingBatches?: number;
21
22
  minReplayLengthMs?: number;
22
23
  sampling?: recordOptions<eventWithTime>["sampling"];
23
24
  slimDOMOptions?: SlimDOMOptions;
@@ -76,6 +77,12 @@ class ReplayTracker {
76
77
  private compressionSupported = false;
77
78
  private readonly sessionSamplingSeed: number;
78
79
  private readonly minReplayLengthMs: number;
80
+ private readonly maxPendingBatches: number;
81
+ private flushScheduled = false;
82
+ private flushTimeout: ReturnType<typeof setTimeout> | null = null;
83
+ private idleFlushId: number | null = null;
84
+ private pendingRetryTimer: ReturnType<typeof setTimeout> | null = null;
85
+ private isProcessingPending = false;
79
86
 
80
87
  constructor(options: ReplayTrackerOptions) {
81
88
  this.siteKey = options.siteKey;
@@ -118,6 +125,7 @@ class ReplayTracker {
118
125
  this.checkoutEveryNth = options.checkoutEveryNth;
119
126
  this.recordConsole = options.recordConsole ?? true;
120
127
  this.minReplayLengthMs = options.minReplayLengthMs ?? 3000;
128
+ this.maxPendingBatches = options.maxPendingBatches ?? 30;
121
129
  this.sessionSamplingSeed = Math.random() * 100;
122
130
 
123
131
  if (typeof window !== "undefined") {
@@ -195,6 +203,8 @@ class ReplayTracker {
195
203
 
196
204
  this.stopRecording?.();
197
205
  this.stopRecording = undefined;
206
+ this.clearScheduledFlush();
207
+ this.clearPendingRetryTimer();
198
208
 
199
209
  if (this.flushTimer) {
200
210
  clearInterval(this.flushTimer);
@@ -218,7 +228,7 @@ class ReplayTracker {
218
228
  return;
219
229
  }
220
230
 
221
- this.flush();
231
+ void this.flush();
222
232
  }
223
233
 
224
234
  private handleEvent(event: eventWithTime, isCheckout?: boolean): void {
@@ -240,17 +250,30 @@ class ReplayTracker {
240
250
  }
241
251
 
242
252
  private scheduleFlush(): void {
243
- if (this.isFlushing || this.events.length === 0) return;
253
+ if (this.isFlushing || this.events.length === 0 || this.flushScheduled)
254
+ return;
255
+
256
+ this.flushScheduled = true;
257
+ const runFlush = () => {
258
+ this.flushScheduled = false;
259
+ this.flushTimeout = null;
260
+ this.idleFlushId = null;
261
+ void this.flush();
262
+ };
244
263
 
245
264
  if (typeof window !== "undefined" && "requestIdleCallback" in window) {
246
- window.requestIdleCallback(() => this.flush(), { timeout: 2000 });
247
- } else {
248
- setTimeout(() => this.flush(), 0);
265
+ this.idleFlushId = window.requestIdleCallback(runFlush, {
266
+ timeout: 2000,
267
+ });
268
+ return;
249
269
  }
270
+
271
+ this.flushTimeout = setTimeout(runFlush, 0);
250
272
  }
251
273
 
252
274
  private handleUnload = (): void => {
253
- this.flush();
275
+ this.clearScheduledFlush();
276
+ void this.flush({ lowLatency: true });
254
277
  };
255
278
 
256
279
  private handleVisibilityChange = (): void => {
@@ -259,7 +282,8 @@ class ReplayTracker {
259
282
  clearInterval(this.flushTimer);
260
283
  this.flushTimer = null;
261
284
  }
262
- this.flush();
285
+ this.clearScheduledFlush();
286
+ void this.flush({ lowLatency: true });
263
287
  } else if (document.visibilityState === "visible" && this.started) {
264
288
  if (!this.flushTimer) {
265
289
  this.flushTimer = setInterval(() => {
@@ -269,9 +293,49 @@ class ReplayTracker {
269
293
  }
270
294
  };
271
295
 
272
- private async flush(): Promise<void> {
296
+ private clearScheduledFlush(): void {
297
+ if (
298
+ this.idleFlushId !== null &&
299
+ typeof window !== "undefined" &&
300
+ "cancelIdleCallback" in window
301
+ ) {
302
+ window.cancelIdleCallback(this.idleFlushId);
303
+ }
304
+ this.idleFlushId = null;
305
+
306
+ if (this.flushTimeout) {
307
+ clearTimeout(this.flushTimeout);
308
+ this.flushTimeout = null;
309
+ }
310
+
311
+ this.flushScheduled = false;
312
+ }
313
+
314
+ private clearPendingRetryTimer(): void {
315
+ if (this.pendingRetryTimer) {
316
+ clearTimeout(this.pendingRetryTimer);
317
+ this.pendingRetryTimer = null;
318
+ }
319
+ }
320
+
321
+ private queuePendingBatch(batch: PendingBatch): void {
322
+ if (this.pendingBatches.length >= this.maxPendingBatches) {
323
+ this.pendingBatches.shift();
324
+ }
325
+ this.pendingBatches.push(batch);
326
+ }
327
+
328
+ private schedulePendingRetry(delayMs: number): void {
329
+ if (this.pendingRetryTimer) return;
330
+ this.pendingRetryTimer = setTimeout(() => {
331
+ this.pendingRetryTimer = null;
332
+ void this.processPendingBatches();
333
+ }, delayMs);
334
+ }
335
+
336
+ private async flush(options: { lowLatency?: boolean } = {}): Promise<void> {
273
337
  if (this.events.length === 0) {
274
- this.processPendingBatches();
338
+ void this.processPendingBatches();
275
339
  return;
276
340
  }
277
341
 
@@ -281,7 +345,7 @@ class ReplayTracker {
281
345
  `[Replay] Too short (${Date.now() - this.startTime}ms), skipping`,
282
346
  );
283
347
  }
284
- this.processPendingBatches();
348
+ void this.processPendingBatches();
285
349
  return;
286
350
  }
287
351
 
@@ -302,6 +366,17 @@ class ReplayTracker {
302
366
  events: eventsToSend,
303
367
  };
304
368
 
369
+ if (this.pendingBatches.length > 0 || this.isProcessingPending) {
370
+ this.queuePendingBatch({
371
+ batch,
372
+ isCompressed: this.compressionSupported && !options.lowLatency,
373
+ retries: 0,
374
+ });
375
+ this.isFlushing = false;
376
+ void this.processPendingBatches();
377
+ return;
378
+ }
379
+
305
380
  if (this.debug) {
306
381
  console.log(
307
382
  `[Replay] Sending ${eventsToSend.length} events (seq: ${batch.sequence})`,
@@ -312,7 +387,7 @@ class ReplayTracker {
312
387
  let compressed: Blob;
313
388
  let isCompressed = false;
314
389
 
315
- if (this.compressionSupported) {
390
+ if (this.compressionSupported && !options.lowLatency) {
316
391
  try {
317
392
  compressed = await this.compress(JSON.stringify(batch));
318
393
  isCompressed = true;
@@ -330,9 +405,11 @@ class ReplayTracker {
330
405
  });
331
406
  }
332
407
 
333
- const success = await this.send(compressed, isCompressed);
408
+ const success = await this.send(compressed, isCompressed, {
409
+ useBeacon: options.lowLatency === true,
410
+ });
334
411
  if (!success) {
335
- this.pendingBatches.push({
412
+ this.queuePendingBatch({
336
413
  batch,
337
414
  isCompressed,
338
415
  retries: 0,
@@ -342,67 +419,78 @@ class ReplayTracker {
342
419
  if (this.debug) {
343
420
  console.warn("[Replay] Flush error:", error);
344
421
  }
345
- this.pendingBatches.push({
422
+ this.queuePendingBatch({
346
423
  batch,
347
424
  isCompressed: false,
348
425
  retries: 0,
349
426
  });
350
427
  } finally {
351
428
  this.isFlushing = false;
352
- this.processPendingBatches();
429
+ void this.processPendingBatches();
353
430
  }
354
431
  }
355
432
 
356
433
  private async processPendingBatches(): Promise<void> {
357
- if (this.pendingBatches.length === 0) return;
358
-
359
- const batch = this.pendingBatches.shift();
360
- if (!batch) return;
434
+ if (this.isProcessingPending || this.pendingBatches.length === 0) return;
435
+ this.clearPendingRetryTimer();
436
+ this.isProcessingPending = true;
361
437
 
362
- if (batch.retries >= 3) {
363
- if (this.debug) {
364
- console.warn(
365
- `[Replay] Max retries reached, restoring batch ${batch.batch.sequence} to buffer`,
366
- );
367
- }
368
- this.events = [...batch.batch.events, ...this.events];
369
- this.scheduleFlush();
370
- this.processPendingBatches();
371
- return;
372
- }
438
+ try {
439
+ while (this.pendingBatches.length > 0) {
440
+ const batch = this.pendingBatches[0];
441
+ if (!batch) {
442
+ break;
443
+ }
373
444
 
374
- batch.retries++;
445
+ if (batch.retries >= 3) {
446
+ if (this.debug) {
447
+ console.warn(
448
+ `[Replay] Max retries reached, restoring batch ${batch.batch.sequence} to buffer`,
449
+ );
450
+ }
451
+ this.pendingBatches.shift();
452
+ this.events = [...batch.batch.events, ...this.events];
453
+ this.scheduleFlush();
454
+ continue;
455
+ }
375
456
 
376
- try {
377
- let blob: Blob;
378
- if (batch.isCompressed && this.compressionSupported) {
379
- try {
380
- blob = await this.compress(JSON.stringify(batch.batch));
381
- } catch {
457
+ batch.retries++;
458
+
459
+ let blob: Blob;
460
+ if (batch.isCompressed && this.compressionSupported) {
461
+ try {
462
+ blob = await this.compress(JSON.stringify(batch.batch));
463
+ } catch {
464
+ blob = new Blob([JSON.stringify(batch.batch)], {
465
+ type: "application/json",
466
+ });
467
+ batch.isCompressed = false;
468
+ }
469
+ } else {
382
470
  blob = new Blob([JSON.stringify(batch.batch)], {
383
471
  type: "application/json",
384
472
  });
385
- batch.isCompressed = false;
386
473
  }
387
- } else {
388
- blob = new Blob([JSON.stringify(batch.batch)], {
389
- type: "application/json",
474
+
475
+ const success = await this.send(blob, batch.isCompressed, {
476
+ useBeacon: false,
390
477
  });
391
- }
478
+ if (success) {
479
+ this.pendingBatches.shift();
480
+ continue;
481
+ }
392
482
 
393
- const success = await this.send(blob, batch.isCompressed);
394
- if (!success) {
395
- this.pendingBatches.push(batch);
396
- setTimeout(() => this.processPendingBatches(), 1000 * batch.retries);
397
- } else {
398
- this.processPendingBatches();
483
+ this.schedulePendingRetry(1000 * batch.retries);
484
+ break;
399
485
  }
400
486
  } catch {
401
- if (this.debug) {
402
- console.warn(`[Replay] Retry ${batch.retries} failed`);
487
+ const current = this.pendingBatches[0];
488
+ if (this.debug && current) {
489
+ console.warn(`[Replay] Retry ${current.retries} failed`);
403
490
  }
404
- this.pendingBatches.push(batch);
405
- setTimeout(() => this.processPendingBatches(), 1000 * batch.retries);
491
+ this.schedulePendingRetry(current ? 1000 * current.retries : 1000);
492
+ } finally {
493
+ this.isProcessingPending = false;
406
494
  }
407
495
  }
408
496
 
@@ -449,7 +537,11 @@ class ReplayTracker {
449
537
  return new Blob([compressed], { type: "application/octet-stream" });
450
538
  }
451
539
 
452
- private async send(data: Blob, isCompressed: boolean): Promise<boolean> {
540
+ private async send(
541
+ data: Blob,
542
+ isCompressed: boolean,
543
+ options: { useBeacon?: boolean } = {},
544
+ ): Promise<boolean> {
453
545
  const url = isCompressed ? `${this.endpoint}?encoding=gzip` : this.endpoint;
454
546
  const sizeKB = (data.size / 1024).toFixed(1);
455
547
 
@@ -463,6 +555,7 @@ class ReplayTracker {
463
555
  headers: isCompressed ? { "Content-Encoding": "gzip" } : undefined,
464
556
  debug: this.debug,
465
557
  debugPrefix: `[Replay] ${sizeKB}KB`,
558
+ useBeacon: options.useBeacon ?? false,
466
559
  }) ?? Promise.resolve(false)
467
560
  );
468
561
  }
@@ -5,6 +5,7 @@ export interface SendDataOptions {
5
5
  headers?: Record<string, string>;
6
6
  debug?: boolean;
7
7
  debugPrefix?: string;
8
+ useBeacon?: boolean;
8
9
  }
9
10
 
10
11
  export function normalizeSamplingPercentage(value: number | undefined): number {
package/src/web-vitals.ts CHANGED
@@ -150,14 +150,16 @@ class WebVitalsTracker {
150
150
  }),
151
151
  );
152
152
 
153
+ const body = JSON.stringify({
154
+ sessionId: getOrCreateSessionId(),
155
+ vitals,
156
+ metadata: {
157
+ url: window.location.href,
158
+ },
159
+ });
160
+
153
161
  return {
154
- body: JSON.stringify({
155
- sessionId: getOrCreateSessionId(),
156
- vitals,
157
- metadata: {
158
- url: window.location.href,
159
- },
160
- }),
162
+ body,
161
163
  count: vitals.length,
162
164
  };
163
165
  }
@@ -174,6 +176,7 @@ class WebVitalsTracker {
174
176
  `[WebVitals] Sending final metrics (${payload.count}): ${names}`,
175
177
  );
176
178
  }
179
+ if (typeof fetch !== "function") return;
177
180
 
178
181
  fetch(this.endpoint, {
179
182
  method: "POST",
@@ -1,5 +1,10 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
- import { getInstance, WebAnalytics } from "../src/analytics";
2
+ import {
3
+ getInstance,
4
+ sendData,
5
+ setConsentMode,
6
+ WebAnalytics,
7
+ } from "../src/analytics";
3
8
 
4
9
  class MockStorage {
5
10
  private readonly store = new Map<string, string>();
@@ -68,6 +73,9 @@ type BrowserHarness = {
68
73
  search: string;
69
74
  };
70
75
  history: History;
76
+ navigator: {
77
+ sendBeacon: (url: string, data?: BodyInit | null) => boolean;
78
+ };
71
79
  };
72
80
 
73
81
  function wait(ms: number): Promise<void> {
@@ -202,6 +210,7 @@ function setupBrowser(): BrowserHarness {
202
210
  fetchCalls,
203
211
  location: locationState,
204
212
  history,
213
+ navigator,
205
214
  restore() {
206
215
  setGlobal("window", originalValues.window);
207
216
  setGlobal("document", originalValues.document);
@@ -251,6 +260,47 @@ describe("WebAnalytics lifecycle", () => {
251
260
  expect(harness.documentTarget.listenerCount("click")).toBe(0);
252
261
  });
253
262
 
263
+ test("sendData uses beacon before fetch", async () => {
264
+ harness.navigator.sendBeacon = () => true;
265
+ const sent = await sendData({
266
+ url: "https://example.com/v1/web",
267
+ data: '{"ok":true}',
268
+ });
269
+ expect(sent).toBe(true);
270
+ expect(harness.fetchCalls.length).toBe(0);
271
+ });
272
+
273
+ test("sendData returns false when fetch is unavailable", async () => {
274
+ harness.navigator.sendBeacon = () => false;
275
+ setGlobal("fetch", undefined as unknown as typeof fetch);
276
+ const sent = await sendData({
277
+ url: "https://example.com/v1/web",
278
+ data: '{"ok":true}',
279
+ });
280
+ expect(sent).toBe(false);
281
+ });
282
+
283
+ test("sendData can skip beacon and use fetch", async () => {
284
+ harness.navigator.sendBeacon = () => true;
285
+ const sent = await sendData({
286
+ url: "https://example.com/v1/web",
287
+ data: '{"ok":true}',
288
+ useBeacon: false,
289
+ });
290
+ expect(sent).toBe(true);
291
+ expect(harness.fetchCalls.length).toBe(1);
292
+ });
293
+
294
+ test("module consent updates apply before instance starts", () => {
295
+ setConsentMode("denied");
296
+ const analytics = new WebAnalytics({
297
+ siteKey: "site_test",
298
+ autoTrack: false,
299
+ consent: { mode: "granted" },
300
+ });
301
+ expect(analytics.getConsentMode()).toBe("denied");
302
+ });
303
+
254
304
  test("consent changes unblock identify payloads", async () => {
255
305
  const analytics = new WebAnalytics({
256
306
  siteKey: "site_test",
@@ -0,0 +1,77 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import {
3
+ getOrCreateAnonymousId,
4
+ getOrCreateSessionId,
5
+ getSessionStart,
6
+ resetSessionId,
7
+ } from "../src/utils/identifiers";
8
+
9
+ class MockStorage {
10
+ private readonly data = new Map<string, string>();
11
+
12
+ getItem(key: string): string | null {
13
+ return this.data.get(key) ?? null;
14
+ }
15
+
16
+ setItem(key: string, value: string): void {
17
+ this.data.set(key, value);
18
+ }
19
+
20
+ removeItem(key: string): void {
21
+ this.data.delete(key);
22
+ }
23
+ }
24
+
25
+ function setGlobal(name: keyof typeof globalThis, value: unknown): void {
26
+ Object.defineProperty(globalThis, name, {
27
+ configurable: true,
28
+ writable: true,
29
+ value,
30
+ });
31
+ }
32
+
33
+ const original = {
34
+ localStorage: globalThis.localStorage,
35
+ sessionStorage: globalThis.sessionStorage,
36
+ };
37
+
38
+ beforeEach(() => {
39
+ setGlobal("localStorage", new MockStorage());
40
+ setGlobal("sessionStorage", new MockStorage());
41
+ });
42
+
43
+ afterEach(() => {
44
+ setGlobal("localStorage", original.localStorage);
45
+ setGlobal("sessionStorage", original.sessionStorage);
46
+ });
47
+
48
+ describe("identifiers", () => {
49
+ test("anonymous id is generated and persisted", () => {
50
+ const id = getOrCreateAnonymousId();
51
+ const second = getOrCreateAnonymousId();
52
+ expect(id.length).toBeGreaterThan(0);
53
+ expect(second).toBe(id);
54
+ });
55
+
56
+ test("session id is recreated when timestamp is invalid", () => {
57
+ const storage = globalThis.sessionStorage as unknown as MockStorage;
58
+ storage.setItem("session_id", "old-session");
59
+ storage.setItem("session_timestamp", "NaN");
60
+ const id = getOrCreateSessionId();
61
+ expect(id).not.toBe("old-session");
62
+ });
63
+
64
+ test("session start falls back when stored value is invalid", () => {
65
+ const storage = globalThis.sessionStorage as unknown as MockStorage;
66
+ storage.setItem("session_start", "NaN");
67
+ const start = getSessionStart();
68
+ expect(Number.isNaN(start)).toBe(true);
69
+ });
70
+
71
+ test("resetSessionId creates a new session id", () => {
72
+ const first = getOrCreateSessionId();
73
+ const second = resetSessionId();
74
+ expect(second.length).toBeGreaterThan(0);
75
+ expect(second).not.toBe(first);
76
+ });
77
+ });
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import ReplayTracker from "../src/replay";
3
+
4
+ describe("ReplayTracker performance", () => {
5
+ test("event ingestion stays within budget", () => {
6
+ const tracker = new ReplayTracker({
7
+ siteKey: "site_test",
8
+ maxEvents: 20000,
9
+ minReplayLengthMs: Number.MAX_SAFE_INTEGER,
10
+ }) as unknown as {
11
+ handleEvent: (event: unknown, isCheckout?: boolean) => void;
12
+ events: unknown[];
13
+ };
14
+
15
+ const startedAt = performance.now();
16
+ for (let i = 0; i < 10000; i++) {
17
+ tracker.handleEvent({ type: 2, timestamp: i }, false);
18
+ }
19
+ const elapsed = performance.now() - startedAt;
20
+
21
+ expect(tracker.events.length).toBe(10000);
22
+ expect(elapsed).toBeLessThan(500);
23
+ });
24
+ });
@@ -0,0 +1,217 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import ReplayTracker from "../src/replay";
3
+
4
+ class MockStorage {
5
+ private readonly data = new Map<string, string>();
6
+
7
+ getItem(key: string): string | null {
8
+ return this.data.get(key) ?? null;
9
+ }
10
+
11
+ setItem(key: string, value: string): void {
12
+ this.data.set(key, value);
13
+ }
14
+
15
+ removeItem(key: string): void {
16
+ this.data.delete(key);
17
+ }
18
+ }
19
+
20
+ function setGlobal(name: keyof typeof globalThis, value: unknown): void {
21
+ Object.defineProperty(globalThis, name, {
22
+ configurable: true,
23
+ writable: true,
24
+ value,
25
+ });
26
+ }
27
+
28
+ const original = {
29
+ window: globalThis.window,
30
+ document: globalThis.document,
31
+ location: globalThis.location,
32
+ navigator: globalThis.navigator,
33
+ fetch: globalThis.fetch,
34
+ localStorage: globalThis.localStorage,
35
+ sessionStorage: globalThis.sessionStorage,
36
+ };
37
+
38
+ beforeEach(() => {
39
+ setGlobal("window", {
40
+ location: { href: "https://example.com/" },
41
+ addEventListener: () => {},
42
+ removeEventListener: () => {},
43
+ } as unknown as Window);
44
+ setGlobal("document", {
45
+ visibilityState: "visible",
46
+ addEventListener: () => {},
47
+ removeEventListener: () => {},
48
+ } as unknown as Document);
49
+ setGlobal("location", { href: "https://example.com/" } as Location);
50
+ setGlobal("localStorage", new MockStorage());
51
+ setGlobal("sessionStorage", new MockStorage());
52
+ setGlobal("navigator", { sendBeacon: () => false });
53
+ setGlobal(
54
+ "fetch",
55
+ (async () => new Response("", { status: 204 })) as unknown as typeof fetch,
56
+ );
57
+ });
58
+
59
+ afterEach(() => {
60
+ setGlobal("window", original.window);
61
+ setGlobal("document", original.document);
62
+ setGlobal("location", original.location);
63
+ setGlobal("navigator", original.navigator);
64
+ setGlobal("fetch", original.fetch);
65
+ setGlobal("localStorage", original.localStorage);
66
+ setGlobal("sessionStorage", original.sessionStorage);
67
+ });
68
+
69
+ describe("ReplayTracker", () => {
70
+ test("collects replay events", () => {
71
+ const tracker = new ReplayTracker({
72
+ siteKey: "site_test",
73
+ maxEvents: 1000,
74
+ minReplayLengthMs: Number.MAX_SAFE_INTEGER,
75
+ }) as unknown as {
76
+ handleEvent: (event: unknown, isCheckout?: boolean) => void;
77
+ events: unknown[];
78
+ };
79
+
80
+ for (let i = 0; i < 12; i++) {
81
+ tracker.handleEvent({ id: i, timestamp: i }, false);
82
+ }
83
+
84
+ expect(tracker.events.length).toBe(12);
85
+ });
86
+
87
+ test("keeps pending batches for retry", () => {
88
+ const tracker = new ReplayTracker({
89
+ siteKey: "site_test",
90
+ }) as unknown as {
91
+ pendingBatches: unknown[];
92
+ };
93
+
94
+ tracker.pendingBatches.push({
95
+ batch: {
96
+ token: "site_test",
97
+ sessionId: "s1",
98
+ sequence: 1,
99
+ timestamp: 1,
100
+ url: "https://example.com/",
101
+ events: [],
102
+ },
103
+ isCompressed: false,
104
+ retries: 0,
105
+ });
106
+ expect(tracker.pendingBatches.length).toBe(1);
107
+ });
108
+
109
+ test("deduplicates scheduled flush work", () => {
110
+ let idleCallbackRuns = 0;
111
+ setGlobal("window", {
112
+ location: { href: "https://example.com/" },
113
+ addEventListener: () => {},
114
+ removeEventListener: () => {},
115
+ requestIdleCallback: () => {
116
+ idleCallbackRuns++;
117
+ return 1;
118
+ },
119
+ cancelIdleCallback: () => {},
120
+ } as unknown as Window);
121
+
122
+ const tracker = new ReplayTracker({
123
+ siteKey: "site_test",
124
+ }) as unknown as {
125
+ events: unknown[];
126
+ scheduleFlush: () => void;
127
+ };
128
+
129
+ tracker.events = [{ type: 2 }, { type: 3 }];
130
+ tracker.scheduleFlush();
131
+ tracker.scheduleFlush();
132
+ expect(idleCallbackRuns).toBe(1);
133
+ });
134
+
135
+ test("caps pending batch buffer size", () => {
136
+ const tracker = new ReplayTracker({
137
+ siteKey: "site_test",
138
+ maxPendingBatches: 3,
139
+ }) as unknown as {
140
+ pendingBatches: unknown[];
141
+ queuePendingBatch: (batch: unknown) => void;
142
+ };
143
+
144
+ for (let i = 0; i < 5; i++) {
145
+ tracker.queuePendingBatch({
146
+ batch: {
147
+ token: "site_test",
148
+ sessionId: "s1",
149
+ sequence: i,
150
+ timestamp: i,
151
+ url: "https://example.com/",
152
+ events: [],
153
+ },
154
+ isCompressed: false,
155
+ retries: 0,
156
+ });
157
+ }
158
+
159
+ expect(tracker.pendingBatches.length).toBe(3);
160
+ });
161
+
162
+ test("queues new flush behind pending batches to preserve order", async () => {
163
+ const tracker = new ReplayTracker({
164
+ siteKey: "site_test",
165
+ minReplayLengthMs: 0,
166
+ }) as unknown as {
167
+ events: unknown[];
168
+ pendingBatches: Array<{
169
+ batch: {
170
+ token: string;
171
+ sessionId: string;
172
+ sequence: number;
173
+ timestamp: number;
174
+ url: string;
175
+ events: unknown[];
176
+ };
177
+ isCompressed: boolean;
178
+ retries: number;
179
+ }>;
180
+ sequenceNumber: number;
181
+ flush: (options?: { lowLatency?: boolean }) => Promise<void>;
182
+ send: (
183
+ data: Blob,
184
+ isCompressed: boolean,
185
+ options?: { useBeacon?: boolean },
186
+ ) => Promise<boolean>;
187
+ };
188
+
189
+ tracker.sequenceNumber = 2;
190
+ tracker.pendingBatches.push({
191
+ batch: {
192
+ token: "site_test",
193
+ sessionId: "s1",
194
+ sequence: 1,
195
+ timestamp: 1,
196
+ url: "https://example.com/",
197
+ events: [{ id: "older" }],
198
+ },
199
+ isCompressed: false,
200
+ retries: 0,
201
+ });
202
+ tracker.events = [{ id: "newer" }];
203
+
204
+ const sentSequences: number[] = [];
205
+ tracker.send = async (data) => {
206
+ const payload = JSON.parse(await data.text()) as { sequence: number };
207
+ sentSequences.push(payload.sequence);
208
+ return true;
209
+ };
210
+
211
+ await tracker.flush();
212
+ await new Promise((resolve) => setTimeout(resolve, 0));
213
+
214
+ expect(sentSequences).toEqual([1, 2]);
215
+ expect(tracker.pendingBatches.length).toBe(0);
216
+ });
217
+ });
@@ -1 +0,0 @@
1
- import{a as e,c as t,f as n,i as r,l as i,n as a,o,r as s,s as c,t as l,u}from"./types-8FXsUqbi.js";let d=null;function f(){return d}function p(e,t){typeof window>`u`||b()||d?.track(e,t??{})}function m(e,t,n){typeof window>`u`||b()||d?.identify(e,t,n??{})}function h(e=!0){typeof window>`u`||b()||d?.logout(e)}function g(e){d?.setConsentMode(e)}function _(){g(`granted`)}function v(){g(`denied`)}function y(e){typeof window>`u`||b()||d?.reportError(e)}function b(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function x(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 S(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function C(){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 w=class{webEndpoint;baseUrl;debug;started=!1;destroyed=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;cleanupCallbacks=[];childTrackers=[];pendingReportedErrors=[];handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.leavePage(),this.stopHeartbeat()):this.startHeartbeat()};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(e){this.options=e,this.baseUrl=u(e.baseUrl),this.webEndpoint=n(this.baseUrl),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(b()){this.log(`disabled`);return}d=this,t(this.isCookielessMode()),setTimeout(()=>void this.start(),0)}}registerCleanup(e){this.cleanupCallbacks.push(e)}addWindowListener(e,t){window.addEventListener(e,t),this.registerCleanup(()=>window.removeEventListener(e,t))}addDocumentListener(e,t){document.addEventListener(e,t),this.registerCleanup(()=>document.removeEventListener(e,t))}patchHistory(){let e=history.pushState.bind(history),t=history.replaceState.bind(history);history.pushState=(t,n,r)=>{e(t,n,r),this.navigate()},history.replaceState=(e,n,r)=>{t(e,n,r),this.navigate()},this.registerCleanup(()=>{history.pushState=e,history.replaceState=t})}ensureStarted(){return typeof window>`u`||this.destroyed||b()?!1:(this.started||this.start(),!0)}stopHeartbeat(){this.heartbeatTimer&&=(clearInterval(this.heartbeatTimer),null)}stopNavigationTimer(){this.navTimer&&=(clearTimeout(this.navTimer),null)}stopChildTrackers(){for(let e of this.childTrackers.splice(0))e.stop?.()}getErrorTracker(){for(let e of this.childTrackers)if(`captureError`in e)return e;return null}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(d&&d!==this){this.log(`already started by another instance`);return}if(b()){this.log(`disabled`);return}this.started=!0,d=this,t(this.isCookielessMode());let e=this.options;try{if(e.trackErrors){let{default:t}=await import(`./error-am04dkiT.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug});for(n.start(),this.childTrackers.push(n);this.pendingReportedErrors.length>0;){let e=this.pendingReportedErrors.shift();e&&n.captureError(e)}this.log(`error loaded`)}if(e.trackWebVitals){let{default:t}=await import(`./web-vitals-Bj8FoiTR.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:l(e.webVitals?.sampling?.percentage)});n.start(),this.childTrackers.push(n),this.log(`web-vitals loaded`)}if(e.trackReplay){let{default:t}=await import(`./replay-TDHY7cyS.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,...e.replayOptions});n.start(),this.childTrackers.push(n),this.log(`replay loaded`)}}catch(e){this.log(`failed to initialize trackers: ${String(e)}`),this.destroy();return}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),this.addDocumentListener(`visibilitychange`,this.handleVisibilityChange),this.addWindowListener(`pagehide`,this.handlePageHide),this.addWindowListener(`popstate`,this.handlePopState),e.trackHash&&this.addWindowListener(`hashchange`,this.handleHashChange),this.patchHistory()}destroy(){if(!this.destroyed){for(this.started&&typeof window<`u`&&this.leavePage(),this.pendingReportedErrors.length=0,this.stopNavigationTimer(),this.stopHeartbeat(),this.scrollHandler&&typeof window<`u`&&(window.removeEventListener(`scroll`,this.scrollHandler),this.scrollHandler=null);this.cleanupCallbacks.length>0;)this.cleanupCallbacks.pop()?.();this.stopChildTrackers(),d===this&&(d=null),this.started=!1,this.destroyed=!0}}pageview(e={}){if(!this.ensureStarted())return;let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.ensureStarted()&&this.send(e,t)}identify(e,t,n={}){if(!this.ensureStarted()||this.isCookielessMode())return;let r=e.trim(),o=t.trim();!r||!o||x({url:i(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:a(!1),externalId:r,email:o,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){this.ensureStarted()&&(e&&o(this.isCookielessMode()),c())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return a(this.isCookielessMode())}getSessionId(){return s()}reportError(e){if(this.destroyed||typeof window>`u`||b()||!(this.options.trackErrors??!1)||!this.ensureStarted())return;let t=this.getErrorTracker();if(t){t.captureError(e);return}this.pendingReportedErrors.length>=50&&this.pendingReportedErrors.shift(),this.pendingReportedErrors.push(e)}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){if(typeof window>`u`||this.destroyed||b())return;let n=a(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:s(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...C(),...t}});this.log(e),x({url:this.webEndpoint,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.destroyed||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-r()})}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.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){this.stopHeartbeat();return}e()},300*1e3)}navigate(){!this.started||this.destroyed||(this.stopNavigationTimer(),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))}links(){let e=e=>{let t=S(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{h as a,y as c,p as d,b as i,x as l,f as n,_ as o,m as r,v as s,w as t,g as u};
@@ -1,2 +0,0 @@
1
- import{f as e,n as t,r as n}from"./types-8FXsUqbi.js";import{l as r}from"./analytics-DO2MFOy_.js";const i=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;function a(e){if(e)return e.split(`
2
- `).map(e=>e.trim()).filter(e=>e.length>0)}function o(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:a(e.stack),cause:o(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function s(e){return e?`${e.error}\0${e.message??``}\0${s(e.cause)}`:``}function c(e){let t=[e.type,e.handled?`handled`:`unhandled`,e.message,e.filename??``,e.lineno??``,s(e.cause)].join(`\0`),n=2166136261,r=3598710387;for(let e=0;e<t.length;e++){let i=t.charCodeAt(e);n=Math.imul(n^i,16777619),r=Math.imul(r^i,2246822519)}return`err_${(n>>>0).toString(16).padStart(8,`0`)}${(r>>>0).toString(16).padStart(8,`0`)}`}var l=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;handledErrors=new WeakSet;errorCounts=new Map;flushTimer=null;started=!1;constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.flushInterval=t.flushInterval??5e3,this.maxQueueSize=t.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`))}skipDuplicate(e){return e instanceof Error?this.handledErrors.has(e)?!0:(this.handledErrors.add(e),!1):!1}handleErrorEvent=e=>{let t=e.error;if(this.skipDuplicate(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.queueError({message:e.message||(t instanceof Error?t.message:`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t instanceof Error?t.stack:void 0,type:`error`,handled:!1,cause:t instanceof Error?o(t.cause):void 0})};handleRejection=e=>{let t=e.reason;if(this.skipDuplicate(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t instanceof Error?t.message:t);return}let n=t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`;this.queueError({message:n,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`,handled:!1,cause:t instanceof Error?o(t.cause):void 0})};queueError(e){if(i.test(`${e.filename??``}\n${e.stack??``}`))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=c(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:a(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}};this.errorCounts.set(t,{entry:n,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.skipDuplicate(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate:`,e.message);return}this.queueError({message:e.message,stack:e.stack,type:`error`,handled:!0,cause:o(e.cause)})}flush(){if(this.errorCounts.size===0)return;let e=[];for(let[t,{entry:n,count:r}]of this.errorCounts)e.push({...n,hash:t,count:r});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,e);let i=t(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim().length>0?a.buildId:void 0,s=JSON.stringify({token:this.siteKey,...i?{userId:i}:{},sessionId:n(),...o?{buildId:o}:{},data:{url:location.href,page:location.pathname,referrer:document.referrer||null,title:document.title},errors:e});this.debug&&console.log(`[ErrorTracker] Payload:`,s),r({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{l as default};
@@ -1 +0,0 @@
1
- import{d as e,n as t,r as n,t as r}from"./types-8FXsUqbi.js";import{l as i}from"./analytics-DO2MFOy_.js";import{getRecordConsolePlugin as a}from"@rrweb/rrweb-plugin-console-record";import{getRecordSequentialIdPlugin as o}from"@rrweb/rrweb-plugin-sequential-id-record";import{record as s}from"rrweb";var c=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(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.samplingPercentage=r(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.minReplayLengthMs=t.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),e.plugins=[o({key:`_faststatsSeqId`}),...this.recordConsole?[a()]:[]],this.stopRecording=s(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 e=this.events;this.events=[];let r=t(),i={token:this.siteKey,sessionId:n(),...r?{identifier:r}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:e};this.debug&&console.log(`[Replay] Sending ${e.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, restoring batch ${e.batch.sequence} to buffer`),this.events=[...e.batch.events,...this.events],this.scheduleFlush(),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 i({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 n()}};export{c as default};