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