@faststats/web 0.1.5 → 0.1.6

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,11 @@
1
1
  # @faststats/web
2
2
 
3
+ ## 0.1.6
4
+
5
+ ### Patch Changes
6
+
7
+ - 48a1008: refactor: perf & bulletproof parts of the core sdk
8
+
3
9
  ## 0.1.5
4
10
 
5
11
  ### 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]`}=e;if(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-DP65ICdP.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-DveDhM7v.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-c6ZVX6pa.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;
@@ -453,6 +454,7 @@ declare class WebAnalytics {
453
454
  private readonly cleanupCallbacks;
454
455
  private readonly childTrackers;
455
456
  private readonly pendingReportedErrors;
457
+ private errorTracker;
456
458
  private readonly handleVisibilityChange;
457
459
  private readonly handlePageHide;
458
460
  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-c6ZVX6pa.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-c6ZVX6pa.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};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)||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)){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){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};
@@ -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.6",
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 {
@@ -133,14 +138,31 @@ export async function sendData(options: SendDataOptions): Promise<boolean> {
133
138
  debugPrefix = "[Analytics]",
134
139
  } = options;
135
140
 
136
- const blob =
137
- data instanceof Blob ? data : new Blob([data], { type: contentType });
141
+ const canUseBeacon =
142
+ typeof navigator !== "undefined" &&
143
+ typeof navigator.sendBeacon === "function";
144
+ if (canUseBeacon) {
145
+ try {
146
+ const beaconBody =
147
+ data instanceof Blob
148
+ ? data
149
+ : typeof Blob !== "undefined"
150
+ ? new Blob([data], { type: contentType })
151
+ : data;
152
+ if (navigator.sendBeacon(url, beaconBody)) {
153
+ if (debug) {
154
+ console.log(`${debugPrefix} Sent via beacon`);
155
+ }
156
+ return true;
157
+ }
158
+ } catch {}
159
+ }
138
160
 
139
- if (navigator.sendBeacon?.(url, blob)) {
161
+ if (typeof fetch !== "function") {
140
162
  if (debug) {
141
- console.log(`${debugPrefix} Sent via beacon`);
163
+ console.warn(`${debugPrefix} Failed to send`);
142
164
  }
143
- return true;
165
+ return false;
144
166
  }
145
167
 
146
168
  try {
@@ -216,6 +238,7 @@ export class WebAnalytics {
216
238
  private readonly cleanupCallbacks: Array<() => void> = [];
217
239
  private readonly childTrackers: ChildTracker[] = [];
218
240
  private readonly pendingReportedErrors: Error[] = [];
241
+ private errorTracker: ErrorTracker | null = null;
219
242
  private readonly handleVisibilityChange = (): void => {
220
243
  if (document.visibilityState === "hidden") {
221
244
  this.leavePage();
@@ -241,6 +264,10 @@ export class WebAnalytics {
241
264
  this.consentMode = options.consent?.mode ?? "granted";
242
265
  this.cookielessWhilePending =
243
266
  options.consent?.cookielessWhilePending ?? true;
267
+ if (pendingConsentMode !== undefined) {
268
+ this.consentMode = pendingConsentMode;
269
+ pendingConsentMode = undefined;
270
+ }
244
271
  if (options.autoTrack ?? true) this.init();
245
272
  }
246
273
 
@@ -330,13 +357,11 @@ export class WebAnalytics {
330
357
  for (const tracker of this.childTrackers.splice(0)) {
331
358
  tracker.stop?.();
332
359
  }
360
+ this.errorTracker = null;
333
361
  }
334
362
 
335
363
  private getErrorTracker(): ErrorTracker | null {
336
- for (const t of this.childTrackers) {
337
- if ("captureError" in t) return t as ErrorTracker;
338
- }
339
- return null;
364
+ return this.errorTracker;
340
365
  }
341
366
 
342
367
  async start(): Promise<void> {
@@ -355,8 +380,8 @@ export class WebAnalytics {
355
380
  setCookielessMode(this.isCookielessMode());
356
381
 
357
382
  const opts = this.options;
358
- try {
359
- if (opts.trackErrors) {
383
+ if (opts.trackErrors) {
384
+ try {
360
385
  const { default: ErrorTrackerClass } = await import("./error");
361
386
  const errorTracker: ErrorTracker = new ErrorTrackerClass({
362
387
  siteKey: opts.siteKey,
@@ -364,14 +389,19 @@ export class WebAnalytics {
364
389
  debug: this.debug,
365
390
  });
366
391
  errorTracker.start();
392
+ this.errorTracker = errorTracker;
367
393
  this.childTrackers.push(errorTracker);
368
394
  while (this.pendingReportedErrors.length > 0) {
369
395
  const pending = this.pendingReportedErrors.shift();
370
396
  if (pending) errorTracker.captureError(pending);
371
397
  }
372
398
  this.log("error loaded");
399
+ } catch (error) {
400
+ this.log(`failed to initialize error tracker: ${String(error)}`);
373
401
  }
374
- if (opts.trackWebVitals) {
402
+ }
403
+ if (opts.trackWebVitals) {
404
+ try {
375
405
  const { default: WebVitalsTrackerClass } = await import("./web-vitals");
376
406
  const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
377
407
  siteKey: opts.siteKey,
@@ -384,8 +414,12 @@ export class WebAnalytics {
384
414
  webVitalsTracker.start();
385
415
  this.childTrackers.push(webVitalsTracker);
386
416
  this.log("web-vitals loaded");
417
+ } catch (error) {
418
+ this.log(`failed to initialize web-vitals tracker: ${String(error)}`);
387
419
  }
388
- if (opts.trackReplay) {
420
+ }
421
+ if (opts.trackReplay) {
422
+ try {
389
423
  const { default: ReplayTrackerClass } = await import("./replay");
390
424
  const replayTracker: ReplayTracker = new ReplayTrackerClass({
391
425
  siteKey: opts.siteKey,
@@ -396,11 +430,9 @@ export class WebAnalytics {
396
430
  replayTracker.start();
397
431
  this.childTrackers.push(replayTracker);
398
432
  this.log("replay loaded");
433
+ } catch (error) {
434
+ this.log(`failed to initialize replay tracker: ${String(error)}`);
399
435
  }
400
- } catch (error) {
401
- this.log(`failed to initialize trackers: ${String(error)}`);
402
- this.destroy();
403
- return;
404
436
  }
405
437
 
406
438
  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
 
@@ -312,7 +376,7 @@ class ReplayTracker {
312
376
  let compressed: Blob;
313
377
  let isCompressed = false;
314
378
 
315
- if (this.compressionSupported) {
379
+ if (this.compressionSupported && !options.lowLatency) {
316
380
  try {
317
381
  compressed = await this.compress(JSON.stringify(batch));
318
382
  isCompressed = true;
@@ -332,7 +396,7 @@ class ReplayTracker {
332
396
 
333
397
  const success = await this.send(compressed, isCompressed);
334
398
  if (!success) {
335
- this.pendingBatches.push({
399
+ this.queuePendingBatch({
336
400
  batch,
337
401
  isCompressed,
338
402
  retries: 0,
@@ -342,67 +406,76 @@ class ReplayTracker {
342
406
  if (this.debug) {
343
407
  console.warn("[Replay] Flush error:", error);
344
408
  }
345
- this.pendingBatches.push({
409
+ this.queuePendingBatch({
346
410
  batch,
347
411
  isCompressed: false,
348
412
  retries: 0,
349
413
  });
350
414
  } finally {
351
415
  this.isFlushing = false;
352
- this.processPendingBatches();
416
+ void this.processPendingBatches();
353
417
  }
354
418
  }
355
419
 
356
420
  private async processPendingBatches(): Promise<void> {
357
- if (this.pendingBatches.length === 0) return;
358
-
359
- const batch = this.pendingBatches.shift();
360
- if (!batch) return;
421
+ if (this.isProcessingPending || this.pendingBatches.length === 0) return;
422
+ this.clearPendingRetryTimer();
423
+ this.isProcessingPending = true;
361
424
 
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
- }
425
+ try {
426
+ while (this.pendingBatches.length > 0) {
427
+ const batch = this.pendingBatches[0];
428
+ if (!batch) {
429
+ break;
430
+ }
373
431
 
374
- batch.retries++;
432
+ if (batch.retries >= 3) {
433
+ if (this.debug) {
434
+ console.warn(
435
+ `[Replay] Max retries reached, restoring batch ${batch.batch.sequence} to buffer`,
436
+ );
437
+ }
438
+ this.pendingBatches.shift();
439
+ this.events = [...batch.batch.events, ...this.events];
440
+ this.scheduleFlush();
441
+ continue;
442
+ }
375
443
 
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 {
444
+ batch.retries++;
445
+
446
+ let blob: Blob;
447
+ if (batch.isCompressed && this.compressionSupported) {
448
+ try {
449
+ blob = await this.compress(JSON.stringify(batch.batch));
450
+ } catch {
451
+ blob = new Blob([JSON.stringify(batch.batch)], {
452
+ type: "application/json",
453
+ });
454
+ batch.isCompressed = false;
455
+ }
456
+ } else {
382
457
  blob = new Blob([JSON.stringify(batch.batch)], {
383
458
  type: "application/json",
384
459
  });
385
- batch.isCompressed = false;
386
460
  }
387
- } else {
388
- blob = new Blob([JSON.stringify(batch.batch)], {
389
- type: "application/json",
390
- });
391
- }
392
461
 
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();
462
+ const success = await this.send(blob, batch.isCompressed);
463
+ if (success) {
464
+ this.pendingBatches.shift();
465
+ continue;
466
+ }
467
+
468
+ this.schedulePendingRetry(1000 * batch.retries);
469
+ break;
399
470
  }
400
471
  } catch {
401
- if (this.debug) {
402
- console.warn(`[Replay] Retry ${batch.retries} failed`);
472
+ const current = this.pendingBatches[0];
473
+ if (this.debug && current) {
474
+ console.warn(`[Replay] Retry ${current.retries} failed`);
403
475
  }
404
- this.pendingBatches.push(batch);
405
- setTimeout(() => this.processPendingBatches(), 1000 * batch.retries);
476
+ this.schedulePendingRetry(current ? 1000 * current.retries : 1000);
477
+ } finally {
478
+ this.isProcessingPending = false;
406
479
  }
407
480
  }
408
481
 
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,36 @@ 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("module consent updates apply before instance starts", () => {
284
+ setConsentMode("denied");
285
+ const analytics = new WebAnalytics({
286
+ siteKey: "site_test",
287
+ autoTrack: false,
288
+ consent: { mode: "granted" },
289
+ });
290
+ expect(analytics.getConsentMode()).toBe("denied");
291
+ });
292
+
254
293
  test("consent changes unblock identify payloads", async () => {
255
294
  const analytics = new WebAnalytics({
256
295
  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,161 @@
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
+ });
@@ -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};