@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 +6 -0
- package/dist/analytics-c6ZVX6pa.js +1 -0
- package/dist/error-DP65ICdP.js +2 -0
- package/dist/module.d.ts +2 -0
- package/dist/module.js +1 -1
- package/dist/replay-DveDhM7v.js +1 -0
- package/dist/{web-vitals-Bj8FoiTR.js → web-vitals-C7H69Xv3.js} +1 -1
- package/package.json +3 -1
- package/scripts/check-bundle-size.mjs +15 -0
- package/src/analytics.ts +50 -18
- package/src/error.ts +13 -4
- package/src/replay.ts +125 -52
- package/src/web-vitals.ts +10 -7
- package/tests/analytics.test.ts +40 -1
- package/tests/identifiers.test.ts +77 -0
- package/tests/replay-stress.test.ts +24 -0
- package/tests/replay.test.ts +161 -0
- package/dist/analytics-DO2MFOy_.js +0 -1
- package/dist/error-am04dkiT.js +0 -2
- package/dist/replay-TDHY7cyS.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -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-
|
|
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.
|
|
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
|
|
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
|
|
137
|
-
|
|
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 (
|
|
161
|
+
if (typeof fetch !== "function") {
|
|
140
162
|
if (debug) {
|
|
141
|
-
console.
|
|
163
|
+
console.warn(`${debugPrefix} Failed to send`);
|
|
142
164
|
}
|
|
143
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
359
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
360
|
-
if (!batch) return;
|
|
421
|
+
if (this.isProcessingPending || this.pendingBatches.length === 0) return;
|
|
422
|
+
this.clearPendingRetryTimer();
|
|
423
|
+
this.isProcessingPending = true;
|
|
361
424
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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
|
-
|
|
402
|
-
|
|
472
|
+
const current = this.pendingBatches[0];
|
|
473
|
+
if (this.debug && current) {
|
|
474
|
+
console.warn(`[Replay] Retry ${current.retries} failed`);
|
|
403
475
|
}
|
|
404
|
-
this.
|
|
405
|
-
|
|
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
|
|
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",
|
package/tests/analytics.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
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};
|
package/dist/error-am04dkiT.js
DELETED
|
@@ -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};
|
package/dist/replay-TDHY7cyS.js
DELETED
|
@@ -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};
|