@faststats/web 0.2.5 → 0.2.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-Ct-lghlf.js +1 -0
- package/dist/{error-uRd9K5Py.js → error-asweBGYd.js} +1 -1
- package/dist/module.d.ts +5 -0
- package/dist/module.js +1 -1
- package/dist/replay-DUA5c3Jf.js +1 -0
- package/package.json +1 -1
- package/src/analytics.ts +83 -49
- package/src/replay.ts +60 -24
- package/tests/replay.test.ts +78 -2
- package/dist/analytics-CEej2XLw.js +0 -1
- package/dist/replay-DkZI1bwa.js +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{a as e,c as t,d as n,f as r,i,l as a,m as o,n as s,o as c,r as l,s as u,t as d,u as f}from"./types-Df70G0eM.js";async function p(e,t){if(t.projectToken&&t.projectId)throw Error(`provide either projectToken or projectId, not both`);if(t.identifier&&t.externalId)throw Error(`provide either serverId or externalId, not both`);let n=t.projectToken,r=t.projectId,i=t.identifier,o=t.externalId;if(!n&&!r)throw Error(`feature flag check requires projectToken or projectId`);if(!i&&!o)throw Error(`feature flag check requires serverId or externalId`);let s={key:e,...r?{projectId:r}:{},...i?{identifier:i}:{externalId:o}};t.attributes&&Object.keys(t.attributes).length>0&&(s.attributes=t.attributes);let c={"Content-Type":`application/json`};n&&(c.Authorization=`Bearer ${n}`);let l=a(t.baseUrl),u=await fetch(l,{method:`POST`,headers:c,body:JSON.stringify(s),credentials:`omit`,signal:t.signal});if(!u.ok){let e=``;try{let t=await u.json();typeof t.error==`string`&&t.error.length>0&&(e=` — ${t.error}`)}catch{}throw Error(`feature flag check failed: HTTP ${u.status}${e}`)}return await u.json()}function m(e){return e.replayOptions?.samplingPercentage!==void 0||e.sessionReplays?.sampling?.percentage!==void 0}function h(e,t,n){if(!(e.sessionReplays?.enabled??e.trackReplay??m(e)))return null;let r=e.replayOptions??{};return{siteKey:e.siteKey,baseUrl:t,debug:n,...r,samplingPercentage:d(r.samplingPercentage??e.sessionReplays?.sampling?.percentage)}}let g=null,_;function v(){return g}function y(e,t){typeof window>`u`||E()||g?.track(e,t??{})}function b(e,t,n){typeof window>`u`||E()||g?.identify(e,t,n??{})}function x(e=!0){typeof window>`u`||E()||g?.logout(e)}function S(e){if(g){g.setConsentMode(e);return}_=e}function C(){S(`granted`)}function w(){S(`denied`)}function T(e){typeof window>`u`||E()||g?.reportError(e)}function E(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function D(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0,keepalive:c=!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:c}),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 O(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function k(){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 A=class{webEndpoint;baseUrl;featureFlagsBaseUrl;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=n(e.baseUrl),this.featureFlagsBaseUrl=r(e.featureFlagsBaseUrl),this.webEndpoint=o(this.baseUrl),this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,_!==void 0&&(this.consentMode=_,_=void 0),(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(E()){this.log(`disabled`);return}g=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||E()?!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}registerChildTracker(e){return!this.started||this.destroyed||g!==this?(e.stop?.(),!1):(this.childTrackers.push(e),!0)}async startErrorTracker(){try{let{default:e}=await import(`./error-asweBGYd.js`);if(!this.started||this.destroyed||g!==this)return;let t=new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug});if(t.start(),!this.registerChildTracker(t))return;for(this.errorTracker=t;this.pendingReportedErrors.length>0;){let e=this.pendingReportedErrors.shift();e&&t.captureError(e)}this.log(`error loaded`)}catch(e){this.log(`failed to initialize error tracker: ${String(e)}`)}}async startWebVitalsTracker(){try{let{default:e}=await import(`./web-vitals-gcUR-TX_.js`);if(!this.started||this.destroyed||g!==this)return;let t=new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:d(this.options.webVitals?.sampling?.percentage)});if(t.start(),!this.registerChildTracker(t))return;this.log(`web-vitals loaded`)}catch(e){this.log(`failed to initialize web-vitals tracker: ${String(e)}`)}}async startReplayTracker(e){try{let{default:t}=await import(`./replay-DUA5c3Jf.js`);if(!this.started||this.destroyed||g!==this)return;let n=new t(e);if(n.start(),!this.registerChildTracker(n))return;this.log(`replay loaded`)}catch(e){this.log(`failed to initialize replay tracker: ${String(e)}`)}}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(g&&g!==this){this.log(`already started by another instance`);return}if(E()){this.log(`disabled`);return}this.started=!0,g=this,t(this.isCookielessMode()),l();let e=this.options,n=h(e,this.baseUrl,this.debug);n&&this.startReplayTracker(n),e.trackErrors&&this.startErrorTracker(),e.trackWebVitals&&this.startWebVitalsTracker(),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(),g===this&&(g=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(),i=t.trim();!r||!i||D({url:f(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:s(!1),externalId:r,email:i,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&&c(this.isCookielessMode()),u())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return s(this.isCookielessMode())}getSessionId(){return l()}async checkFeatureFlag(e,t,n){if(typeof window>`u`||E())return{value:`false`};let r=n?.externalId?.trim();if(r)return p(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,externalId:r,attributes:t,signal:n?.signal});let i=this.getAnonymousId();return i?p(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,identifier:i,attributes:t,signal:n?.signal}):{value:`false`}}reportError(e){if(this.destroyed||typeof window>`u`||E()||!(this.options.trackErrors??!1)||!this.ensureStarted())return;let t=this.errorTracker;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||E())return;let n=s(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:l(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...k(),...t}});this.log(e),D({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-i()})}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=O(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{x as a,T as c,y as d,p as f,E as i,D as l,v as n,C as o,b as r,w as s,A as t,S as u};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{m as e,n as t,r as n}from"./types-Df70G0eM.js";import{l as r}from"./analytics-
|
|
1
|
+
import{m as e,n as t,r as n}from"./types-Df70G0eM.js";import{l as r}from"./analytics-Ct-lghlf.js";const i=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//,a=e=>e?.split(`
|
|
2
2
|
`).map(e=>e.trim()).filter(Boolean);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.kind,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;handled=new WeakSet;queue=new Map;timer=null;started=!1;constructor(t){this.options=t,this.endpoint=e(t.baseUrl)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??5e3}get maxQueueSize(){return this.options.maxQueueSize??50}log(...e){this.debug&&console.log(`[ErrorTracker]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.onError),window.addEventListener(`unhandledrejection`,this.onRejection),document.addEventListener(`visibilitychange`,this.onVisibilityChange),window.addEventListener(`pagehide`,this.flush),this.timer=setInterval(this.flush,this.flushInterval),this.log(`Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.onError),window.removeEventListener(`unhandledrejection`,this.onRejection),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),window.removeEventListener(`pagehide`,this.flush),this.timer&&clearInterval(this.timer),this.timer=null,this.flush(),this.log(`Stopped listening for errors`))}captureError(e){this.record({kind:`error`,message:e.message,stack:e.stack,handled:!0,cause:o(e.cause)},e)}onError=e=>{let t=e.error;this.record({kind:`error`,message:e.message||`Unknown error`,filename:e.filename||void 0,lineno:e.lineno||void 0,stack:t instanceof Error?t.stack:void 0,handled:!1,cause:t instanceof Error?o(t.cause):void 0},t)};onRejection=e=>{let t=e.reason;this.record({kind:`unhandledrejection`,message:t.message||`Unhandled promise rejection`,stack:t.stack||void 0,handled:!1,cause:o(t.cause)},t)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush()};record(e,t){if(t instanceof Error){if(this.handled.has(t)){this.log(`Skipping duplicate:`,t.message);return}this.handled.add(t)}if(i.test(`${e.filename??``}\n${e.stack??``}`))return;let n=c(e),r=this.queue.get(n);r?r.count+=1:this.queue.set(n,{hash:n,count:1,error:e.kind===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:a(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}}),this.log(`Captured error:`,e),this.queue.size>=this.maxQueueSize&&this.flush()}flush=()=>{if(this.queue.size===0)return;let e=[...this.queue.values()];this.queue.clear();let i=t(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim()?a.buildId:void 0,s=JSON.stringify({token:this.options.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.log(`Flushing errors:`,e),this.log(`Payload:`,s),r({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{l as default};
|
package/dist/module.d.ts
CHANGED
|
@@ -374,6 +374,7 @@ interface ReplayTrackerOptions {
|
|
|
374
374
|
siteKey: string;
|
|
375
375
|
baseUrl?: string;
|
|
376
376
|
debug?: boolean;
|
|
377
|
+
compress?: boolean;
|
|
377
378
|
samplingPercentage?: number;
|
|
378
379
|
flushInterval?: number;
|
|
379
380
|
maxEvents?: number;
|
|
@@ -492,6 +493,10 @@ declare class WebAnalytics {
|
|
|
492
493
|
private stopHeartbeat;
|
|
493
494
|
private stopNavigationTimer;
|
|
494
495
|
private stopChildTrackers;
|
|
496
|
+
private registerChildTracker;
|
|
497
|
+
private startErrorTracker;
|
|
498
|
+
private startWebVitalsTracker;
|
|
499
|
+
private startReplayTracker;
|
|
495
500
|
start(): Promise<void>;
|
|
496
501
|
destroy(): void;
|
|
497
502
|
pageview(extra?: Dict): void;
|
package/dist/module.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{f as e,l as t}from"./types-Df70G0eM.js";import{a as n,c as r,d as i,f as a,i as o,l as s,n as c,o as l,r as u,s as d,t as f,u as p}from"./analytics-
|
|
1
|
+
import{f as e,l as t}from"./types-Df70G0eM.js";import{a as n,c as r,d as i,f as a,i as o,l as s,n as c,o as l,r as u,s as d,t as f,u as p}from"./analytics-Ct-lghlf.js";export{f as WebAnalytics,t as featureFlagsCheckUrl,a as fetchFeatureFlagEvaluation,c as getInstance,u as identify,o as isTrackingDisabled,n as logout,e as normalizeFeatureFlagsBaseUrl,l as optIn,d as optOut,r as reportError,s as sendData,p as setConsentMode,i as trackEvent};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{i as e,n as t,p as n,r,t as i}from"./types-Df70G0eM.js";import{l as a}from"./analytics-Ct-lghlf.js";import{getRecordConsolePlugin as o}from"@rrweb/rrweb-plugin-console-record";import{getRecordSequentialIdPlugin as s}from"@rrweb/rrweb-plugin-sequential-id-record";import{EventType as c,record as l}from"rrweb";const u={mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},d={script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0};var f=class{endpoint;compressionSupported=typeof window<`u`&&`CompressionStream`in window;sampled;events=[];pending=[];sessionId;started=!1;startTime=0;sequence=0;intervalId=null;flushTask=null;retryTask=null;minLengthFlushTask=null;stopRecording;sending=!1;constructor(e){this.options=e,this.endpoint=n(e.baseUrl),this.sampled=Math.random()*100<i(e.samplingPercentage)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??1e4}get maxEvents(){return this.options.maxEvents??500}get maxPendingBatches(){return this.options.maxPendingBatches??30}get minReplayLengthMs(){return this.options.minReplayLengthMs??3e3}get shouldCompress(){return this.options.compress??!1}log(...e){this.debug&&console.log(`[Replay]`,...e)}start(){this.started||typeof window>`u`||!this.sampled||(this.started=!0,this.sessionId=r(),this.startTime=e(),this.stopRecording=l({emit:this.onEvent,sampling:this.options.sampling??u,slimDOMOptions:this.options.slimDOMOptions??d,maskAllInputs:this.options.maskAllInputs??!0,maskInputOptions:this.options.maskInputOptions??{password:!0,email:!0,tel:!0},blockClass:this.options.blockClass,blockSelector:this.options.blockSelector,maskTextClass:this.options.maskTextClass,maskTextSelector:this.options.maskTextSelector,checkoutEveryNms:this.options.checkoutEveryNms??6e4,checkoutEveryNth:this.options.checkoutEveryNth,plugins:[s({key:`_faststatsSeqId`}),...this.options.recordConsole??!0?[o()]:[]]}),this.intervalId=setInterval(this.requestFlush,this.flushInterval),window.addEventListener(`beforeunload`,this.onUnload),window.addEventListener(`pagehide`,this.onUnload),document.addEventListener(`visibilitychange`,this.onVisibilityChange),this.log(`Recording started`))}stop(){if(this.started){if(this.started=!1,this.stopRecording?.(),this.stopRecording=void 0,this.intervalId&&clearInterval(this.intervalId),this.flushTask&&clearTimeout(this.flushTask),this.retryTask&&clearTimeout(this.retryTask),this.minLengthFlushTask&&clearTimeout(this.minLengthFlushTask),this.intervalId=null,this.flushTask=null,this.retryTask=null,this.minLengthFlushTask=null,window.removeEventListener(`beforeunload`,this.onUnload),window.removeEventListener(`pagehide`,this.onUnload),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),!this.hasReachedMinLength()){this.events.length=0,this.sessionId=void 0,this.log(`Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush(!0),this.sessionId=void 0,this.log(`Recording stopped`)}}getSessionId(){return this.sessionId??r()}onEvent=(e,t)=>{if(this.events.push(e),t||this.events.length>=this.maxEvents||e.type===c.FullSnapshot&&this.hasReachedMinLength()){this.requestFlush();return}this.scheduleMinLengthFlush()};onUnload=()=>{this.flush(!0)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush(!0)};hasReachedMinLength(){return this.minReplayLengthMs<=0||Date.now()-this.startTime>=this.minReplayLengthMs}requestFlush=()=>{this.flushTask||this.sending||this.events.length===0||(this.minLengthFlushTask&&=(clearTimeout(this.minLengthFlushTask),null),this.flushTask=setTimeout(()=>{this.flushTask=null,this.flush(!1)},0))};scheduleMinLengthFlush(){if(this.minLengthFlushTask||this.events.length===0||this.hasReachedMinLength())return;let e=Math.max(0,this.minReplayLengthMs-(Date.now()-this.startTime)),t=Math.min(e,2147483647);this.minLengthFlushTask=setTimeout(()=>{this.minLengthFlushTask=null,this.requestFlush()},t)}createBatch(e){let n=t();return{token:this.options.siteKey,sessionId:this.getSessionId(),...n?{identifier:n}:{},sequence:this.sequence++,timestamp:Date.now(),url:window.location.href,events:e}}async encodeBatch(e,t){let n=JSON.stringify(e);if(!this.shouldCompress||!this.compressionSupported||t)return{data:n,isCompressed:!1};try{return{data:await this.compress(n),isCompressed:!0}}catch{return this.log(`Compression failed, using uncompressed`),{data:new Blob([n],{type:`application/json`}),isCompressed:!1}}}async flush(e){if(!this.sending){if(this.events.length>0)if(!this.hasReachedMinLength())this.scheduleMinLengthFlush();else{let e=this.createBatch(this.events.splice(0));this.pending.push(e)}if(this.pending.length!==0){this.sending=!0;try{for(;this.pending.length>0;){let t=this.pending[0];if(!t)break;let n=await this.encodeBatch(t,e);if(!await this.send(n.data,n.isCompressed,e)){this.pending.length>=this.maxPendingBatches&&(this.log(`Pending buffer full, dropping batch ${t.sequence}`),this.pending.shift()),this.log(`Failed to send replay batch ${t.sequence}, retrying`),this.scheduleRetry();break}this.pending.shift(),e=!1}}finally{this.sending=!1}}}}scheduleRetry(){this.retryTask||=setTimeout(()=>{this.retryTask=null,this.flush(!1)},1e3)}async compress(e){let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();await r.write(t),await 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;return new Blob([s],{type:`application/octet-stream`})}send(e,t,n){return a({url:t?`${this.endpoint}?encoding=gzip`:this.endpoint,data:e,contentType:t?`application/octet-stream`:`application/json`,debug:!1,useBeacon:!1,keepalive:n})??Promise.resolve(!1)}};export{f as default};
|
package/package.json
CHANGED
package/src/analytics.ts
CHANGED
|
@@ -407,6 +407,81 @@ export class WebAnalytics {
|
|
|
407
407
|
this.errorTracker = null;
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
+
private registerChildTracker(tracker: ChildTracker): boolean {
|
|
411
|
+
if (!this.started || this.destroyed || _instance !== this) {
|
|
412
|
+
tracker.stop?.();
|
|
413
|
+
return false;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
this.childTrackers.push(tracker);
|
|
417
|
+
return true;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
private async startErrorTracker(): Promise<void> {
|
|
421
|
+
try {
|
|
422
|
+
const { default: ErrorTrackerClass } = await import("./error");
|
|
423
|
+
if (!this.started || this.destroyed || _instance !== this) return;
|
|
424
|
+
|
|
425
|
+
const errorTracker: ErrorTracker = new ErrorTrackerClass({
|
|
426
|
+
siteKey: this.options.siteKey,
|
|
427
|
+
baseUrl: this.baseUrl,
|
|
428
|
+
debug: this.debug,
|
|
429
|
+
});
|
|
430
|
+
errorTracker.start();
|
|
431
|
+
if (!this.registerChildTracker(errorTracker)) return;
|
|
432
|
+
|
|
433
|
+
this.errorTracker = errorTracker;
|
|
434
|
+
while (this.pendingReportedErrors.length > 0) {
|
|
435
|
+
const pending = this.pendingReportedErrors.shift();
|
|
436
|
+
if (pending) errorTracker.captureError(pending);
|
|
437
|
+
}
|
|
438
|
+
this.log("error loaded");
|
|
439
|
+
} catch (error) {
|
|
440
|
+
this.log(`failed to initialize error tracker: ${String(error)}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private async startWebVitalsTracker(): Promise<void> {
|
|
445
|
+
try {
|
|
446
|
+
const { default: WebVitalsTrackerClass } = await import("./web-vitals");
|
|
447
|
+
if (!this.started || this.destroyed || _instance !== this) return;
|
|
448
|
+
|
|
449
|
+
const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
|
|
450
|
+
siteKey: this.options.siteKey,
|
|
451
|
+
baseUrl: this.baseUrl,
|
|
452
|
+
debug: this.debug,
|
|
453
|
+
samplingPercentage: normalizeSamplingPercentage(
|
|
454
|
+
this.options.webVitals?.sampling?.percentage,
|
|
455
|
+
),
|
|
456
|
+
});
|
|
457
|
+
webVitalsTracker.start();
|
|
458
|
+
if (!this.registerChildTracker(webVitalsTracker)) return;
|
|
459
|
+
|
|
460
|
+
this.log("web-vitals loaded");
|
|
461
|
+
} catch (error) {
|
|
462
|
+
this.log(`failed to initialize web-vitals tracker: ${String(error)}`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async startReplayTracker(
|
|
467
|
+
replayOptions: ReplayTrackerOptions,
|
|
468
|
+
): Promise<void> {
|
|
469
|
+
try {
|
|
470
|
+
const { default: ReplayTrackerClass } = await import("./replay");
|
|
471
|
+
if (!this.started || this.destroyed || _instance !== this) return;
|
|
472
|
+
|
|
473
|
+
const replayTracker: ReplayTracker = new ReplayTrackerClass(
|
|
474
|
+
replayOptions,
|
|
475
|
+
);
|
|
476
|
+
replayTracker.start();
|
|
477
|
+
if (!this.registerChildTracker(replayTracker)) return;
|
|
478
|
+
|
|
479
|
+
this.log("replay loaded");
|
|
480
|
+
} catch (error) {
|
|
481
|
+
this.log(`failed to initialize replay tracker: ${String(error)}`);
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
410
485
|
async start(): Promise<void> {
|
|
411
486
|
if (this.started || this.destroyed || typeof window === "undefined") return;
|
|
412
487
|
if (_instance && _instance !== this) {
|
|
@@ -421,63 +496,22 @@ export class WebAnalytics {
|
|
|
421
496
|
this.started = true;
|
|
422
497
|
_instance = this;
|
|
423
498
|
setCookielessMode(this.isCookielessMode());
|
|
499
|
+
getOrCreateSessionId();
|
|
424
500
|
|
|
425
501
|
const opts = this.options;
|
|
426
|
-
if (opts.trackErrors) {
|
|
427
|
-
try {
|
|
428
|
-
const { default: ErrorTrackerClass } = await import("./error");
|
|
429
|
-
const errorTracker: ErrorTracker = new ErrorTrackerClass({
|
|
430
|
-
siteKey: opts.siteKey,
|
|
431
|
-
baseUrl: this.baseUrl,
|
|
432
|
-
debug: this.debug,
|
|
433
|
-
});
|
|
434
|
-
errorTracker.start();
|
|
435
|
-
this.errorTracker = errorTracker;
|
|
436
|
-
this.childTrackers.push(errorTracker);
|
|
437
|
-
while (this.pendingReportedErrors.length > 0) {
|
|
438
|
-
const pending = this.pendingReportedErrors.shift();
|
|
439
|
-
if (pending) errorTracker.captureError(pending);
|
|
440
|
-
}
|
|
441
|
-
this.log("error loaded");
|
|
442
|
-
} catch (error) {
|
|
443
|
-
this.log(`failed to initialize error tracker: ${String(error)}`);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
if (opts.trackWebVitals) {
|
|
447
|
-
try {
|
|
448
|
-
const { default: WebVitalsTrackerClass } = await import("./web-vitals");
|
|
449
|
-
const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
|
|
450
|
-
siteKey: opts.siteKey,
|
|
451
|
-
baseUrl: this.baseUrl,
|
|
452
|
-
debug: this.debug,
|
|
453
|
-
samplingPercentage: normalizeSamplingPercentage(
|
|
454
|
-
opts.webVitals?.sampling?.percentage,
|
|
455
|
-
),
|
|
456
|
-
});
|
|
457
|
-
webVitalsTracker.start();
|
|
458
|
-
this.childTrackers.push(webVitalsTracker);
|
|
459
|
-
this.log("web-vitals loaded");
|
|
460
|
-
} catch (error) {
|
|
461
|
-
this.log(`failed to initialize web-vitals tracker: ${String(error)}`);
|
|
462
|
-
}
|
|
463
|
-
}
|
|
464
502
|
const replayOptions = resolveReplayTrackerOptions(
|
|
465
503
|
opts,
|
|
466
504
|
this.baseUrl,
|
|
467
505
|
this.debug,
|
|
468
506
|
);
|
|
469
507
|
if (replayOptions) {
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
this.log("replay loaded");
|
|
478
|
-
} catch (error) {
|
|
479
|
-
this.log(`failed to initialize replay tracker: ${String(error)}`);
|
|
480
|
-
}
|
|
508
|
+
void this.startReplayTracker(replayOptions);
|
|
509
|
+
}
|
|
510
|
+
if (opts.trackErrors) {
|
|
511
|
+
void this.startErrorTracker();
|
|
512
|
+
}
|
|
513
|
+
if (opts.trackWebVitals) {
|
|
514
|
+
void this.startWebVitalsTracker();
|
|
481
515
|
}
|
|
482
516
|
|
|
483
517
|
this.enterPage();
|
package/src/replay.ts
CHANGED
|
@@ -6,15 +6,21 @@ import type { recordOptions } from "rrweb/typings/types";
|
|
|
6
6
|
import type { SlimDOMOptions } from "rrweb-snapshot";
|
|
7
7
|
import { sendData } from "./analytics";
|
|
8
8
|
import { replayEventsUrl } from "./utils/api-urls";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
getAnonymousId,
|
|
11
|
+
getOrCreateSessionId,
|
|
12
|
+
getSessionStart,
|
|
13
|
+
} from "./utils/identifiers";
|
|
10
14
|
import { normalizeSamplingPercentage } from "./utils/types";
|
|
11
15
|
|
|
12
16
|
const RRWEB_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
|
|
17
|
+
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
13
18
|
|
|
14
19
|
export interface ReplayTrackerOptions {
|
|
15
20
|
siteKey: string;
|
|
16
21
|
baseUrl?: string;
|
|
17
22
|
debug?: boolean;
|
|
23
|
+
compress?: boolean;
|
|
18
24
|
samplingPercentage?: number;
|
|
19
25
|
flushInterval?: number;
|
|
20
26
|
maxEvents?: number;
|
|
@@ -72,12 +78,14 @@ export default class ReplayTracker {
|
|
|
72
78
|
private readonly events: eventWithTime[] = [];
|
|
73
79
|
private readonly pending: ReplayBatch[] = [];
|
|
74
80
|
|
|
81
|
+
private sessionId: string | undefined;
|
|
75
82
|
private started = false;
|
|
76
83
|
private startTime = 0;
|
|
77
84
|
private sequence = 0;
|
|
78
85
|
private intervalId: ReturnType<typeof setInterval> | null = null;
|
|
79
86
|
private flushTask: ReturnType<typeof setTimeout> | null = null;
|
|
80
87
|
private retryTask: ReturnType<typeof setTimeout> | null = null;
|
|
88
|
+
private minLengthFlushTask: ReturnType<typeof setTimeout> | null = null;
|
|
81
89
|
private stopRecording?: listenerHandler;
|
|
82
90
|
private sending = false;
|
|
83
91
|
|
|
@@ -108,6 +116,10 @@ export default class ReplayTracker {
|
|
|
108
116
|
return this.options.minReplayLengthMs ?? 3000;
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
private get shouldCompress(): boolean {
|
|
120
|
+
return this.options.compress ?? false;
|
|
121
|
+
}
|
|
122
|
+
|
|
111
123
|
private log(...args: unknown[]): void {
|
|
112
124
|
if (this.debug) console.log("[Replay]", ...args);
|
|
113
125
|
}
|
|
@@ -116,7 +128,8 @@ export default class ReplayTracker {
|
|
|
116
128
|
if (this.started || typeof window === "undefined" || !this.sampled) return;
|
|
117
129
|
|
|
118
130
|
this.started = true;
|
|
119
|
-
this.
|
|
131
|
+
this.sessionId = getOrCreateSessionId();
|
|
132
|
+
this.startTime = getSessionStart();
|
|
120
133
|
|
|
121
134
|
this.stopRecording = record({
|
|
122
135
|
emit: this.onEvent,
|
|
@@ -161,10 +174,12 @@ export default class ReplayTracker {
|
|
|
161
174
|
if (this.intervalId) clearInterval(this.intervalId);
|
|
162
175
|
if (this.flushTask) clearTimeout(this.flushTask);
|
|
163
176
|
if (this.retryTask) clearTimeout(this.retryTask);
|
|
177
|
+
if (this.minLengthFlushTask) clearTimeout(this.minLengthFlushTask);
|
|
164
178
|
|
|
165
179
|
this.intervalId = null;
|
|
166
180
|
this.flushTask = null;
|
|
167
181
|
this.retryTask = null;
|
|
182
|
+
this.minLengthFlushTask = null;
|
|
168
183
|
|
|
169
184
|
window.removeEventListener("beforeunload", this.onUnload);
|
|
170
185
|
window.removeEventListener("pagehide", this.onUnload);
|
|
@@ -172,6 +187,7 @@ export default class ReplayTracker {
|
|
|
172
187
|
|
|
173
188
|
if (!this.hasReachedMinLength()) {
|
|
174
189
|
this.events.length = 0;
|
|
190
|
+
this.sessionId = undefined;
|
|
175
191
|
this.log(
|
|
176
192
|
`Session too short (${Date.now() - this.startTime}ms), discarding events`,
|
|
177
193
|
);
|
|
@@ -179,11 +195,12 @@ export default class ReplayTracker {
|
|
|
179
195
|
}
|
|
180
196
|
|
|
181
197
|
void this.flush(true);
|
|
198
|
+
this.sessionId = undefined;
|
|
182
199
|
this.log("Recording stopped");
|
|
183
200
|
}
|
|
184
201
|
|
|
185
202
|
getSessionId(): string | undefined {
|
|
186
|
-
return getOrCreateSessionId();
|
|
203
|
+
return this.sessionId ?? getOrCreateSessionId();
|
|
187
204
|
}
|
|
188
205
|
|
|
189
206
|
private onEvent = (event: eventWithTime, isCheckout?: boolean): void => {
|
|
@@ -195,7 +212,10 @@ export default class ReplayTracker {
|
|
|
195
212
|
(event.type === EventType.FullSnapshot && this.hasReachedMinLength())
|
|
196
213
|
) {
|
|
197
214
|
this.requestFlush();
|
|
215
|
+
return;
|
|
198
216
|
}
|
|
217
|
+
|
|
218
|
+
this.scheduleMinLengthFlush();
|
|
199
219
|
};
|
|
200
220
|
|
|
201
221
|
private onUnload = (): void => {
|
|
@@ -218,18 +238,44 @@ export default class ReplayTracker {
|
|
|
218
238
|
private requestFlush = (): void => {
|
|
219
239
|
if (this.flushTask || this.sending || this.events.length === 0) return;
|
|
220
240
|
|
|
241
|
+
if (this.minLengthFlushTask) {
|
|
242
|
+
clearTimeout(this.minLengthFlushTask);
|
|
243
|
+
this.minLengthFlushTask = null;
|
|
244
|
+
}
|
|
245
|
+
|
|
221
246
|
this.flushTask = setTimeout(() => {
|
|
222
247
|
this.flushTask = null;
|
|
223
248
|
void this.flush(false);
|
|
224
249
|
}, 0);
|
|
225
250
|
};
|
|
226
251
|
|
|
252
|
+
private scheduleMinLengthFlush(): void {
|
|
253
|
+
if (
|
|
254
|
+
this.minLengthFlushTask ||
|
|
255
|
+
this.events.length === 0 ||
|
|
256
|
+
this.hasReachedMinLength()
|
|
257
|
+
) {
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const remainingMs = Math.max(
|
|
262
|
+
0,
|
|
263
|
+
this.minReplayLengthMs - (Date.now() - this.startTime),
|
|
264
|
+
);
|
|
265
|
+
const timeoutMs = Math.min(remainingMs, MAX_TIMEOUT_MS);
|
|
266
|
+
|
|
267
|
+
this.minLengthFlushTask = setTimeout(() => {
|
|
268
|
+
this.minLengthFlushTask = null;
|
|
269
|
+
this.requestFlush();
|
|
270
|
+
}, timeoutMs);
|
|
271
|
+
}
|
|
272
|
+
|
|
227
273
|
private createBatch(events: eventWithTime[]): ReplayBatch {
|
|
228
274
|
const identifier = getAnonymousId();
|
|
229
275
|
|
|
230
276
|
return {
|
|
231
277
|
token: this.options.siteKey,
|
|
232
|
-
sessionId:
|
|
278
|
+
sessionId: this.getSessionId(),
|
|
233
279
|
...(identifier ? { identifier } : {}),
|
|
234
280
|
sequence: this.sequence++,
|
|
235
281
|
timestamp: Date.now(),
|
|
@@ -241,12 +287,12 @@ export default class ReplayTracker {
|
|
|
241
287
|
private async encodeBatch(
|
|
242
288
|
batch: ReplayBatch,
|
|
243
289
|
lowLatency: boolean,
|
|
244
|
-
): Promise<{ data: Blob; isCompressed: boolean }> {
|
|
290
|
+
): Promise<{ data: string | Blob; isCompressed: boolean }> {
|
|
245
291
|
const json = JSON.stringify(batch);
|
|
246
292
|
|
|
247
|
-
if (!this.compressionSupported || lowLatency) {
|
|
293
|
+
if (!this.shouldCompress || !this.compressionSupported || lowLatency) {
|
|
248
294
|
return {
|
|
249
|
-
data:
|
|
295
|
+
data: json,
|
|
250
296
|
isCompressed: false,
|
|
251
297
|
};
|
|
252
298
|
}
|
|
@@ -270,11 +316,10 @@ export default class ReplayTracker {
|
|
|
270
316
|
|
|
271
317
|
if (this.events.length > 0) {
|
|
272
318
|
if (!this.hasReachedMinLength()) {
|
|
273
|
-
this.
|
|
274
|
-
`Too short (${Date.now() - this.startTime}ms), skipping flush`,
|
|
275
|
-
);
|
|
319
|
+
this.scheduleMinLengthFlush();
|
|
276
320
|
} else {
|
|
277
|
-
this.
|
|
321
|
+
const batch = this.createBatch(this.events.splice(0));
|
|
322
|
+
this.pending.push(batch);
|
|
278
323
|
}
|
|
279
324
|
}
|
|
280
325
|
|
|
@@ -298,11 +343,10 @@ export default class ReplayTracker {
|
|
|
298
343
|
this.log(`Pending buffer full, dropping batch ${batch.sequence}`);
|
|
299
344
|
this.pending.shift();
|
|
300
345
|
}
|
|
346
|
+
this.log(`Failed to send replay batch ${batch.sequence}, retrying`);
|
|
301
347
|
this.scheduleRetry();
|
|
302
348
|
break;
|
|
303
349
|
}
|
|
304
|
-
|
|
305
|
-
this.log(`Sent ${batch.events.length} events (seq: ${batch.sequence})`);
|
|
306
350
|
this.pending.shift();
|
|
307
351
|
lowLatency = false;
|
|
308
352
|
}
|
|
@@ -313,7 +357,6 @@ export default class ReplayTracker {
|
|
|
313
357
|
|
|
314
358
|
private scheduleRetry(): void {
|
|
315
359
|
if (this.retryTask) return;
|
|
316
|
-
|
|
317
360
|
this.retryTask = setTimeout(() => {
|
|
318
361
|
this.retryTask = null;
|
|
319
362
|
void this.flush(false);
|
|
@@ -346,17 +389,11 @@ export default class ReplayTracker {
|
|
|
346
389
|
offset += chunk.length;
|
|
347
390
|
}
|
|
348
391
|
|
|
349
|
-
this.log(
|
|
350
|
-
`Compressed: ${input.length} → ${output.length} bytes (${(
|
|
351
|
-
(output.length / input.length) * 100
|
|
352
|
-
).toFixed(1)}%)`,
|
|
353
|
-
);
|
|
354
|
-
|
|
355
392
|
return new Blob([output], { type: "application/octet-stream" });
|
|
356
393
|
}
|
|
357
394
|
|
|
358
395
|
private send(
|
|
359
|
-
data: Blob,
|
|
396
|
+
data: string | Blob,
|
|
360
397
|
isCompressed: boolean,
|
|
361
398
|
lowLatency: boolean,
|
|
362
399
|
): Promise<boolean> {
|
|
@@ -367,9 +404,8 @@ export default class ReplayTracker {
|
|
|
367
404
|
contentType: isCompressed
|
|
368
405
|
? "application/octet-stream"
|
|
369
406
|
: "application/json",
|
|
370
|
-
debug:
|
|
371
|
-
|
|
372
|
-
useBeacon: lowLatency,
|
|
407
|
+
debug: false,
|
|
408
|
+
useBeacon: false,
|
|
373
409
|
keepalive: lowLatency,
|
|
374
410
|
}) ?? Promise.resolve(false)
|
|
375
411
|
);
|
package/tests/replay.test.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import ReplayTracker from "../src/replay";
|
|
3
|
+
import { getOrCreateSessionId } from "../src/utils/identifiers";
|
|
3
4
|
|
|
4
5
|
class MockStorage {
|
|
5
6
|
private readonly data = new Map<string, string>();
|
|
@@ -36,12 +37,13 @@ type ReplayBatch = {
|
|
|
36
37
|
type ReplayTrackerInternals = {
|
|
37
38
|
events: ReplayEvent[];
|
|
38
39
|
pending: ReplayBatch[];
|
|
40
|
+
minLengthFlushTask: ReturnType<typeof setTimeout> | null;
|
|
39
41
|
retryTask: ReturnType<typeof setTimeout> | null;
|
|
40
42
|
startTime: number;
|
|
41
43
|
onEvent: (event: ReplayEvent, isCheckout?: boolean) => void;
|
|
42
44
|
flush: (lowLatency: boolean) => Promise<void>;
|
|
43
45
|
send: (
|
|
44
|
-
data: Blob,
|
|
46
|
+
data: string | Blob,
|
|
45
47
|
isCompressed: boolean,
|
|
46
48
|
lowLatency: boolean,
|
|
47
49
|
) => Promise<boolean>;
|
|
@@ -136,6 +138,28 @@ describe("ReplayTracker", () => {
|
|
|
136
138
|
expect(sent).toBe(0);
|
|
137
139
|
expect(tracker.events.length).toBe(1);
|
|
138
140
|
expect(tracker.pending.length).toBe(0);
|
|
141
|
+
|
|
142
|
+
if (tracker.minLengthFlushTask) clearTimeout(tracker.minLengthFlushTask);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("flushes buffered events once minimum replay length is reached", async () => {
|
|
146
|
+
const tracker = createTracker({
|
|
147
|
+
minReplayLengthMs: 20,
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
let sent = 0;
|
|
151
|
+
tracker.send = async () => {
|
|
152
|
+
sent++;
|
|
153
|
+
return true;
|
|
154
|
+
};
|
|
155
|
+
tracker.startTime = Date.now();
|
|
156
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
157
|
+
|
|
158
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
159
|
+
|
|
160
|
+
expect(sent).toBe(1);
|
|
161
|
+
expect(tracker.events.length).toBe(0);
|
|
162
|
+
expect(tracker.pending.length).toBe(0);
|
|
139
163
|
});
|
|
140
164
|
|
|
141
165
|
test("emits replay payload when flushed", async () => {
|
|
@@ -146,7 +170,9 @@ describe("ReplayTracker", () => {
|
|
|
146
170
|
let payload: ReplayBatch | null = null;
|
|
147
171
|
let lowLatency = false;
|
|
148
172
|
tracker.send = async (data, _isCompressed, nextLowLatency) => {
|
|
149
|
-
payload = JSON.parse(
|
|
173
|
+
payload = JSON.parse(
|
|
174
|
+
typeof data === "string" ? data : await data.text(),
|
|
175
|
+
) as ReplayBatch;
|
|
150
176
|
lowLatency = nextLowLatency;
|
|
151
177
|
return true;
|
|
152
178
|
};
|
|
@@ -163,6 +189,56 @@ describe("ReplayTracker", () => {
|
|
|
163
189
|
expect(tracker.pending.length).toBe(0);
|
|
164
190
|
});
|
|
165
191
|
|
|
192
|
+
test("keeps the same replay session id across later batches", async () => {
|
|
193
|
+
const tracker = createTracker({
|
|
194
|
+
minReplayLengthMs: 0,
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const payloads: ReplayBatch[] = [];
|
|
198
|
+
tracker.send = async (data) => {
|
|
199
|
+
payloads.push(
|
|
200
|
+
JSON.parse(
|
|
201
|
+
typeof data === "string" ? data : await data.text(),
|
|
202
|
+
) as ReplayBatch,
|
|
203
|
+
);
|
|
204
|
+
return true;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
(tracker as unknown as { sessionId?: string }).sessionId =
|
|
208
|
+
getOrCreateSessionId();
|
|
209
|
+
tracker.startTime = Date.now() - 1000;
|
|
210
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
211
|
+
await tracker.flush(false);
|
|
212
|
+
|
|
213
|
+
const storage = globalThis.sessionStorage as unknown as MockStorage;
|
|
214
|
+
storage.setItem("session_timestamp", "0");
|
|
215
|
+
|
|
216
|
+
tracker.onEvent({ type: 3, timestamp: Date.now(), data: {} }, false);
|
|
217
|
+
await tracker.flush(false);
|
|
218
|
+
|
|
219
|
+
expect(payloads).toHaveLength(2);
|
|
220
|
+
expect(payloads[0]?.sessionId).toBeTruthy();
|
|
221
|
+
expect(payloads[1]?.sessionId).toBe(payloads[0]?.sessionId);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("sends replay as plain JSON by default", async () => {
|
|
225
|
+
const tracker = createTracker({
|
|
226
|
+
minReplayLengthMs: 0,
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
let bodyType: "string" | "blob" | null = null;
|
|
230
|
+
tracker.send = async (data) => {
|
|
231
|
+
bodyType = typeof data === "string" ? "string" : "blob";
|
|
232
|
+
return true;
|
|
233
|
+
};
|
|
234
|
+
tracker.startTime = Date.now() - 1000;
|
|
235
|
+
tracker.onEvent({ type: 2, timestamp: Date.now(), data: {} }, false);
|
|
236
|
+
|
|
237
|
+
await tracker.flush(false);
|
|
238
|
+
|
|
239
|
+
expect(bodyType).toBe("string");
|
|
240
|
+
});
|
|
241
|
+
|
|
166
242
|
test("keeps failed batches queued for retry", async () => {
|
|
167
243
|
const tracker = createTracker({
|
|
168
244
|
minReplayLengthMs: 0,
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{a as e,c as t,d as n,f as r,i,l as a,m as o,n as s,o as c,r as l,s as u,t as d,u as f}from"./types-Df70G0eM.js";async function p(e,t){if(t.projectToken&&t.projectId)throw Error(`provide either projectToken or projectId, not both`);if(t.identifier&&t.externalId)throw Error(`provide either serverId or externalId, not both`);let n=t.projectToken,r=t.projectId,i=t.identifier,o=t.externalId;if(!n&&!r)throw Error(`feature flag check requires projectToken or projectId`);if(!i&&!o)throw Error(`feature flag check requires serverId or externalId`);let s={key:e,...r?{projectId:r}:{},...i?{identifier:i}:{externalId:o}};t.attributes&&Object.keys(t.attributes).length>0&&(s.attributes=t.attributes);let c={"Content-Type":`application/json`};n&&(c.Authorization=`Bearer ${n}`);let l=a(t.baseUrl),u=await fetch(l,{method:`POST`,headers:c,body:JSON.stringify(s),credentials:`omit`,signal:t.signal});if(!u.ok){let e=``;try{let t=await u.json();typeof t.error==`string`&&t.error.length>0&&(e=` — ${t.error}`)}catch{}throw Error(`feature flag check failed: HTTP ${u.status}${e}`)}return await u.json()}function m(e){return e.replayOptions?.samplingPercentage!==void 0||e.sessionReplays?.sampling?.percentage!==void 0}function h(e,t,n){if(!(e.sessionReplays?.enabled??e.trackReplay??m(e)))return null;let r=e.replayOptions??{};return{siteKey:e.siteKey,baseUrl:t,debug:n,...r,samplingPercentage:d(r.samplingPercentage??e.sessionReplays?.sampling?.percentage)}}let g=null,_;function v(){return g}function y(e,t){typeof window>`u`||E()||g?.track(e,t??{})}function b(e,t,n){typeof window>`u`||E()||g?.identify(e,t,n??{})}function x(e=!0){typeof window>`u`||E()||g?.logout(e)}function S(e){if(g){g.setConsentMode(e);return}_=e}function C(){S(`granted`)}function w(){S(`denied`)}function T(e){typeof window>`u`||E()||g?.reportError(e)}function E(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function D(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0,keepalive:c=!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:c}),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 O(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function k(){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 A=class{webEndpoint;baseUrl;featureFlagsBaseUrl;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=n(e.baseUrl),this.featureFlagsBaseUrl=r(e.featureFlagsBaseUrl),this.webEndpoint=o(this.baseUrl),this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,_!==void 0&&(this.consentMode=_,_=void 0),(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(E()){this.log(`disabled`);return}g=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||E()?!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}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(g&&g!==this){this.log(`already started by another instance`);return}if(E()){this.log(`disabled`);return}this.started=!0,g=this,t(this.isCookielessMode());let e=this.options;if(e.trackErrors)try{let{default:t}=await import(`./error-uRd9K5Py.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-gcUR-TX_.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:d(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)}`)}let n=h(e,this.baseUrl,this.debug);if(n)try{let{default:e}=await import(`./replay-DkZI1bwa.js`),t=new e(n);t.start(),this.childTrackers.push(t),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(),g===this&&(g=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(),i=t.trim();!r||!i||D({url:f(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:s(!1),externalId:r,email:i,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&&c(this.isCookielessMode()),u())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return s(this.isCookielessMode())}getSessionId(){return l()}async checkFeatureFlag(e,t,n){if(typeof window>`u`||E())return{value:`false`};let r=n?.externalId?.trim();if(r)return p(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,externalId:r,attributes:t,signal:n?.signal});let i=this.getAnonymousId();return i?p(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,identifier:i,attributes:t,signal:n?.signal}):{value:`false`}}reportError(e){if(this.destroyed||typeof window>`u`||E()||!(this.options.trackErrors??!1)||!this.ensureStarted())return;let t=this.errorTracker;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||E())return;let n=s(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:l(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...k(),...t}});this.log(e),D({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-i()})}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=O(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{x as a,T as c,y as d,p as f,E as i,D as l,v as n,C as o,b as r,w as s,A as t,S as u};
|
package/dist/replay-DkZI1bwa.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e,p as t,r as n,t as r}from"./types-Df70G0eM.js";import{l as i}from"./analytics-CEej2XLw.js";import{getRecordConsolePlugin as a}from"@rrweb/rrweb-plugin-console-record";import{getRecordSequentialIdPlugin as o}from"@rrweb/rrweb-plugin-sequential-id-record";import{EventType as s,record as c}from"rrweb";const l={mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},u={script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0};var d=class{endpoint;compressionSupported=typeof window<`u`&&`CompressionStream`in window;sampled;events=[];pending=[];started=!1;startTime=0;sequence=0;intervalId=null;flushTask=null;retryTask=null;stopRecording;sending=!1;constructor(e){this.options=e,this.endpoint=t(e.baseUrl),this.sampled=Math.random()*100<r(e.samplingPercentage)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??1e4}get maxEvents(){return this.options.maxEvents??500}get maxPendingBatches(){return this.options.maxPendingBatches??30}get minReplayLengthMs(){return this.options.minReplayLengthMs??3e3}log(...e){this.debug&&console.log(`[Replay]`,...e)}start(){this.started||typeof window>`u`||!this.sampled||(this.started=!0,this.startTime=Date.now(),this.stopRecording=c({emit:this.onEvent,sampling:this.options.sampling??l,slimDOMOptions:this.options.slimDOMOptions??u,maskAllInputs:this.options.maskAllInputs??!0,maskInputOptions:this.options.maskInputOptions??{password:!0,email:!0,tel:!0},blockClass:this.options.blockClass,blockSelector:this.options.blockSelector,maskTextClass:this.options.maskTextClass,maskTextSelector:this.options.maskTextSelector,checkoutEveryNms:this.options.checkoutEveryNms??6e4,checkoutEveryNth:this.options.checkoutEveryNth,plugins:[o({key:`_faststatsSeqId`}),...this.options.recordConsole??!0?[a()]:[]]}),this.intervalId=setInterval(this.requestFlush,this.flushInterval),window.addEventListener(`beforeunload`,this.onUnload),window.addEventListener(`pagehide`,this.onUnload),document.addEventListener(`visibilitychange`,this.onVisibilityChange),this.log(`Recording started`))}stop(){if(this.started){if(this.started=!1,this.stopRecording?.(),this.stopRecording=void 0,this.intervalId&&clearInterval(this.intervalId),this.flushTask&&clearTimeout(this.flushTask),this.retryTask&&clearTimeout(this.retryTask),this.intervalId=null,this.flushTask=null,this.retryTask=null,window.removeEventListener(`beforeunload`,this.onUnload),window.removeEventListener(`pagehide`,this.onUnload),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),!this.hasReachedMinLength()){this.events.length=0,this.log(`Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush(!0),this.log(`Recording stopped`)}}getSessionId(){return n()}onEvent=(e,t)=>{this.events.push(e),(t||this.events.length>=this.maxEvents||e.type===s.FullSnapshot&&this.hasReachedMinLength())&&this.requestFlush()};onUnload=()=>{this.flush(!0)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush(!0)};hasReachedMinLength(){return this.minReplayLengthMs<=0||Date.now()-this.startTime>=this.minReplayLengthMs}requestFlush=()=>{this.flushTask||this.sending||this.events.length===0||(this.flushTask=setTimeout(()=>{this.flushTask=null,this.flush(!1)},0))};createBatch(t){let r=e();return{token:this.options.siteKey,sessionId:n(),...r?{identifier:r}:{},sequence:this.sequence++,timestamp:Date.now(),url:window.location.href,events:t}}async encodeBatch(e,t){let n=JSON.stringify(e);if(!this.compressionSupported||t)return{data:new Blob([n],{type:`application/json`}),isCompressed:!1};try{return{data:await this.compress(n),isCompressed:!0}}catch{return this.log(`Compression failed, using uncompressed`),{data:new Blob([n],{type:`application/json`}),isCompressed:!1}}}async flush(e){if(!this.sending&&(this.events.length>0&&(this.hasReachedMinLength()?this.pending.push(this.createBatch(this.events.splice(0))):this.log(`Too short (${Date.now()-this.startTime}ms), skipping flush`)),this.pending.length!==0)){this.sending=!0;try{for(;this.pending.length>0;){let t=this.pending[0];if(!t)break;let n=await this.encodeBatch(t,e);if(!await this.send(n.data,n.isCompressed,e)){this.pending.length>=this.maxPendingBatches&&(this.log(`Pending buffer full, dropping batch ${t.sequence}`),this.pending.shift()),this.scheduleRetry();break}this.log(`Sent ${t.events.length} events (seq: ${t.sequence})`),this.pending.shift(),e=!1}}finally{this.sending=!1}}}scheduleRetry(){this.retryTask||=setTimeout(()=>{this.retryTask=null,this.flush(!1)},1e3)}async compress(e){let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();await r.write(t),await 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;return this.log(`Compressed: ${t.length} → ${s.length} bytes (${(s.length/t.length*100).toFixed(1)}%)`),new Blob([s],{type:`application/octet-stream`})}send(e,t,n){return i({url:t?`${this.endpoint}?encoding=gzip`:this.endpoint,data:e,contentType:t?`application/octet-stream`:`application/json`,debug:this.debug,debugPrefix:`[Replay] ${(e.size/1024).toFixed(1)}KB`,useBeacon:n,keepalive:n})??Promise.resolve(!1)}};export{d as default};
|