@faststats/web 0.2.10 → 0.2.12

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.
Files changed (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/chunks/error-CttYL43D.js +2 -0
  3. package/dist/chunks/identifiers-CQeWm7wi.js +1 -0
  4. package/dist/chunks/replay-BrMLCiBF.js +1 -0
  5. package/dist/chunks/{replay-IhsP2Ab4.d.ts → replay-BuX3_0xs.d.ts} +1 -0
  6. package/dist/chunks/send-data-DL_GlsQw.js +1 -0
  7. package/dist/chunks/types-CYzR5xtT.js +1 -0
  8. package/dist/chunks/web-vitals-CjA1bFLG.js +1 -0
  9. package/dist/error.d.ts +3 -0
  10. package/dist/error.js +1 -1
  11. package/dist/feature-flags.d.ts +1 -1
  12. package/dist/feature-flags.js +1 -1
  13. package/dist/index.d.ts +6 -3
  14. package/dist/index.js +1 -1
  15. package/dist/replay.d.ts +1 -1
  16. package/dist/replay.js +1 -1
  17. package/dist/web-vitals.d.ts +13 -3
  18. package/dist/web-vitals.js +1 -1
  19. package/package.json +1 -1
  20. package/src/analytics.ts +57 -115
  21. package/src/error.ts +38 -16
  22. package/src/replay.ts +13 -2
  23. package/src/utils/identifiers.ts +38 -26
  24. package/src/utils/send-data.ts +52 -0
  25. package/src/web-vitals.ts +157 -49
  26. package/tests/analytics.test.ts +35 -0
  27. package/tests/identifiers.test.ts +18 -0
  28. package/tests/replay.test.ts +25 -0
  29. package/tests/web-vitals.test.ts +208 -0
  30. package/dist/chunks/analytics-ur06JW5D.js +0 -1
  31. package/dist/chunks/error-ClMugucs.js +0 -2
  32. package/dist/chunks/replay-DQHUrcod.js +0 -1
  33. package/dist/chunks/types-C3vW7XGe.js +0 -1
  34. package/dist/chunks/web-vitals-BooBPWJC.js +0 -1
  35. /package/dist/chunks/{feature-flags-6rZlmhfu.d.ts → feature-flags-BxZz_lNm.d.ts} +0 -0
  36. /package/dist/chunks/{feature-flags-CqVtrpX2.js → feature-flags-CjnLZGxp.js} +0 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @faststats/web
2
2
 
3
+ ## 0.2.12
4
+
5
+ ### Patch Changes
6
+
7
+ - d96a750: fix: refreshSessionTimestamp on replay page hide
8
+
9
+ ## 0.2.11
10
+
11
+ ### Patch Changes
12
+
13
+ - 754b67f: fix(replays): dedupe secquences to reduce risk of duplicate data sending
14
+ - d21ce42: refactor: more reliable and unified data sending
15
+
3
16
  ## 0.2.10
4
17
 
5
18
  ## 0.2.9
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{o as t}from"./api-urls-BrkcoElX.js";import{n,t as r}from"./identifiers-CQeWm7wi.js";import{t as i}from"./send-data-DL_GlsQw.js";var a=e({default:()=>d});const o=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//,s=e=>e?.split(`
2
+ `).map(e=>e.trim()).filter(Boolean);function c(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:s(e.stack),cause:c(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function l(e){return e?`${e.error}\0${e.message??``}\0${l(e.cause)}`:``}function u(e){let t=[e.kind,e.handled?`handled`:`unhandled`,e.message,e.filename??``,e.lineno??``,l(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 d=class{endpoint;handled=new WeakSet;queue=new Map;timer=null;started=!1;flushing=!1;constructor(e){this.options=e,this.endpoint=t(e.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.requestFlush),this.timer=setInterval(this.requestFlush,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.requestFlush),this.timer&&clearInterval(this.timer),this.timer=null,this.requestFlush(),this.log(`Stopped listening for errors`))}captureError(e){this.record({kind:`error`,message:e.message,stack:e.stack,handled:!0,cause:c(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?c(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:c(t.cause)},t)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.requestFlush()};requestFlush=()=>{this.flush()};restoreErrors(e){for(let t of e)this.queue.set(t.hash,{...t,count:t.count+(this.queue.get(t.hash)?.count??0)})}record(e,t){if(t instanceof Error){if(this.handled.has(t)){this.log(`Skipping duplicate:`,t.message);return}this.handled.add(t)}if(o.test(`${e.filename??``}\n${e.stack??``}`))return;let n=u(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:s(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}}),this.log(`Captured error:`,e),this.queue.size>=this.maxQueueSize&&this.requestFlush()}async flush(){if(this.flushing||this.queue.size===0)return;let e=[...this.queue.values()];this.queue.clear(),this.flushing=!0;let t=r(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim()?a.buildId:void 0,s=JSON.stringify({token:this.options.siteKey,...t?{userId:t}:{},sessionId:n(),...o?{buildId:o}:{},sdkName:this.options.sdkName??`@faststats/web`,sdkVersion:this.options.sdkVersion??`0.2.12`,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);try{await i({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})||this.restoreErrors(e)}catch{this.restoreErrors(e)}finally{this.flushing=!1}}};export{a as n,d as t};
@@ -0,0 +1 @@
1
+ const e=`faststats_anon_id`,t=`session_id`,n=`session_timestamp`,r=`session_start`;let i=!1;function a(e){i=e}function o(e){return e??i?``:s()}function s(){if(!localStorage)return``;let t=localStorage.getItem(e);if(t)return t;let n=crypto.randomUUID();return localStorage.setItem(e,n),n}function c(t){return t||!localStorage?``:(localStorage.removeItem(e),s())}function l(){if(!sessionStorage)return``;let e=sessionStorage.getItem(t),i=sessionStorage.getItem(n);if(e&&i){if(Date.now()-Number.parseInt(i,10)<18e5)return sessionStorage.setItem(n,Date.now().toString()),e;p(sessionStorage)}let a=Date.now().toString(),o=crypto.randomUUID();return sessionStorage.setItem(t,o),sessionStorage.setItem(n,a),sessionStorage.setItem(r,a),o}function u(){return sessionStorage?(p(sessionStorage),l()):``}function d(){sessionStorage&&sessionStorage.getItem(t)&&sessionStorage.setItem(n,Date.now().toString())}function f(){if(!sessionStorage)return Date.now();let e=sessionStorage.getItem(r);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(n);return t?Number.parseInt(t,10):Date.now()}function p(e){e.removeItem(t),e.removeItem(n),e.removeItem(r)}export{c as a,d as i,l as n,u as o,f as r,a as s,o as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{a as t}from"./api-urls-BrkcoElX.js";import{n,r,t as i}from"./identifiers-CQeWm7wi.js";import{t as a}from"./send-data-DL_GlsQw.js";import{t as o}from"./types-CYzR5xtT.js";import{EventType as s}from"@rrweb/types";import{record as c}from"rrweb";var l=e({default:()=>f});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=[];pendingSizeBytes=0;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=t(e.baseUrl),this.sampled=Math.random()*100<o(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 maxQueueSizeBytes(){return this.options.maxQueueSizeBytes??2097152}get minReplayLengthMs(){return this.options.minReplayLengthMs??3e3}get shouldCompress(){return this.options.compress??!0}log(...e){this.debug&&console.log(`[Replay]`,...e)}start(){this.started||typeof window>`u`||!this.sampled||(this.started=!0,this.sessionId=n(),this.startTime=r(),this.beginRecording(),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`))}async beginRecording(){let e=this.options.recordConsole??!0,[{getRecordSequentialIdPlugin:t},n]=await Promise.all([import(`@rrweb/rrweb-plugin-sequential-id-record`),e?import(`@rrweb/rrweb-plugin-console-record`):Promise.resolve(null)]);if(!this.started)return;let r=[t({key:`_faststatsSeqId`})];n&&r.push(n.getRecordConsolePlugin()),this.stopRecording=c({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:r})}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??n()}onEvent=(e,t)=>{if(this.events.push(e),t||this.events.length>=this.maxEvents||e.type===s.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 t=i(),n=this.sequence++;return{token:this.options.siteKey,sessionId:this.getSessionId(),...t?{identifier:t}:{},batchId:this.createBatchId(n),sequence:n,timestamp:Date.now(),url:window.location.href,events:e}}createBatchId(e){let t=typeof crypto<`u`&&`randomUUID`in crypto?crypto.randomUUID():`${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;return`${this.getSessionId()??`unknown`}-${e}-${t}`}getBatchSizeBytes(e){return new TextEncoder().encode(JSON.stringify(e)).byteLength}dropOldestPendingBatch(e){let t=this.pending.shift();t&&(this.pendingSizeBytes=Math.max(0,this.pendingSizeBytes-this.getBatchSizeBytes(t)),this.log(`${e}, dropping batch ${t.sequence}`))}enqueueBatch(e){let t=this.getBatchSizeBytes(e);if(t>this.maxQueueSizeBytes){this.log(`Replay batch ${e.sequence} is ${t}B, exceeding ${this.maxQueueSizeBytes}B queue limit; dropping`);return}for(;this.pending.length>0&&this.pendingSizeBytes+t>this.maxQueueSizeBytes;)this.dropOldestPendingBatch(`Pending queue size limit reached`);for(;this.pending.length>=this.maxPendingBatches;)this.dropOldestPendingBatch(`Pending batch limit reached`);this.pending.push(e),this.pendingSizeBytes+=t}async encodeBatch(e){let t=JSON.stringify(e);if(!this.shouldCompress||!this.compressionSupported)return{data:t,isCompressed:!1};try{let e=await this.compress(t);return this.log(`Compressed ${t.length}B -> ${e.byteLength}B (${Math.round(e.byteLength/t.length*100)}%)`),{data:e,isCompressed:!0}}catch{return this.log(`Compression failed, using uncompressed`),{data:t,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.enqueueBatch(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);if(!await this.send(n.data,n.isCompressed,e)){this.log(`Failed to send replay batch ${t.sequence}, retrying`),this.scheduleRetry();break}this.dropOldestPendingBatch(`Sent replay batch`),e=!1}}finally{this.sending=!1}}}}scheduleRetry(){this.retryTask||=setTimeout(()=>{this.retryTask=null,this.flush(!1)},1e3)}async compress(e){let t=new Blob([e]).stream().pipeThrough(new CompressionStream(`gzip`)),n=await new Response(t).arrayBuffer();return new Uint8Array(n)}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{l as n,f as t};
@@ -64,6 +64,7 @@ declare class ReplayTracker {
64
64
  private requestFlush;
65
65
  private scheduleMinLengthFlush;
66
66
  private createBatch;
67
+ private createBatchId;
67
68
  private getBatchSizeBytes;
68
69
  private dropOldestPendingBatch;
69
70
  private enqueueBatch;
@@ -0,0 +1 @@
1
+ async function e(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 globalThis.navigator?.sendBeacon==`function`)try{let e=n instanceof Blob?n:typeof Blob<`u`?new Blob([n],{type:r}):n;if(globalThis.navigator.sendBeacon(t,e))return a&&console.log(`${o} Sent via beacon`),!0}catch{}try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:c}),s=e.ok;if(a){let t=s?`${o} Sent via fetch`:`${o} Failed: ${e.status}`;console[s?`log`:`warn`](t)}return s}catch{return a&&console.warn(`${o} Failed to send`),!1}}export{e as t};
@@ -0,0 +1 @@
1
+ function e(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{e as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{s as t}from"./api-urls-BrkcoElX.js";import{n}from"./identifiers-CQeWm7wi.js";import{t as r}from"./types-CYzR5xtT.js";var i=e({default:()=>s});async function a(e){let{onCLS:t,onFCP:n,onINP:r,onLCP:i,onTTFB:a}=e?await import(`web-vitals/attribution`):await import(`web-vitals`);return o([t,n,r,i,a])}function o([e,t,n,r,i]){return[{observe:e,options:{reportAllChanges:!0}},{observe:t},{observe:n,options:{reportAllChanges:!0}},{observe:r,options:{reportAllChanges:!0}},{observe:i}]}var s=class{endpoint;metricsByUrl=new Map;pendingFlushUrls=new Set;sampled;started=!1;flushing=!1;finalFlushRequested=!1;currentUrl=``;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}log(...e){this.debug&&console.log(`[WebVitals]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,this.currentUrl=window.location.href,document.addEventListener(`visibilitychange`,this.onVisibilityChange,{passive:!0}),window.addEventListener(`pagehide`,this.onPageHide,{passive:!0}),this.log(`Tracking started`),this.observe())}stop(){!this.started||typeof window>`u`||(this.started=!1,this.cleanupListeners(),this.queueFlush(this.currentUrl,!0))}trackPageChange(e){if(!this.started||typeof window>`u`)return;let t=e??window.location.href;if(t===this.currentUrl)return;let n=this.currentUrl;this.currentUrl=t,this.queueFlush(n)}cleanupListeners(){document.removeEventListener(`visibilitychange`,this.onVisibilityChange),window.removeEventListener(`pagehide`,this.onPageHide)}async observe(){try{let e=await a(this.options.attribution??!1);if(!this.started)return;for(let{observe:t,options:n}of e)t(this.captureMetric,n)}catch(e){this.log(`Failed to load web-vitals`,e)}}onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.queueFlush(this.currentUrl,!0)};onPageHide=()=>{this.queueFlush(this.currentUrl,!0)};captureMetric=e=>{if(!this.started||!this.sampled)return;let t=e.name,n=e.attribution??void 0;this.metricsForUrl(this.currentUrl).set(t,{value:e.value,attributes:{id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...n??{}}}),this.log(`${t} captured: ${e.value}`)};metricsForUrl(e){let t=this.metricsByUrl.get(e);return t||(t=new Map,this.metricsByUrl.set(e,t)),t}queueFlush(e,t=!1){this.finalFlushRequested||=t,this.pendingFlushUrls.add(e),this.flushPending()}async flushPending(){if(!this.flushing){if(typeof window>`u`||typeof fetch!=`function`){this.finishFinalFlushIfNeeded();return}this.flushing=!0;try{let e=[...this.pendingFlushUrls];this.pendingFlushUrls.clear();for(let t of e)await this.sendMetricsForUrl(t)||this.pendingFlushUrls.add(t)}finally{this.flushing=!1,this.finishFinalFlushIfNeeded()}}}async sendMetricsForUrl(e){let t=this.metricsByUrl.get(e);if(!t||t.size===0)return!0;let r=[...t.entries()].map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes})),i=JSON.stringify({sessionId:n(),vitals:r,metadata:{url:e}});this.log(`Sending metrics for ${e} (${r.length})`);try{let t=new AbortController,n=window.setTimeout(()=>t.abort(),3e3);try{let n=await fetch(this.endpoint,{method:`POST`,body:i,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.options.siteKey}`},keepalive:!0,signal:t.signal});if(!n.ok)throw Error(`HTTP ${n.status}`);this.metricsByUrl.delete(e)}finally{clearTimeout(n)}return!0}catch(e){return this.log(`Failed to send metrics`,e),!1}}finishFinalFlushIfNeeded(){this.finalFlushRequested&&(this.finalFlushRequested=!1,this.started=!1,this.cleanupListeners())}};export{i as n,s as t};
package/dist/error.d.ts CHANGED
@@ -26,6 +26,7 @@ declare class ErrorTracker {
26
26
  private readonly queue;
27
27
  private timer;
28
28
  private started;
29
+ private flushing;
29
30
  constructor(options: ErrorTrackingOptions);
30
31
  private get debug();
31
32
  private get flushInterval();
@@ -37,6 +38,8 @@ declare class ErrorTracker {
37
38
  private onError;
38
39
  private onRejection;
39
40
  private onVisibilityChange;
41
+ private requestFlush;
42
+ private restoreErrors;
40
43
  private record;
41
44
  private flush;
42
45
  }
package/dist/error.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./chunks/error-ClMugucs.js";export{e as default};
1
+ import{t as e}from"./chunks/error-CttYL43D.js";export{e as default};
@@ -1,2 +1,2 @@
1
- import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-6rZlmhfu.js";
1
+ import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-BxZz_lNm.js";
2
2
  export { type FeatureFlagCheckContext, type FeatureFlagEvaluation, fetchFeatureFlagEvaluation };
@@ -1 +1 @@
1
- import{n as e}from"./chunks/feature-flags-CqVtrpX2.js";export{e as fetchFeatureFlagEvaluation};
1
+ import{n as e}from"./chunks/feature-flags-CjnLZGxp.js";export{e as fetchFeatureFlagEvaluation};
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-6rZlmhfu.js";
2
- import { n as ReplayTrackerOptions } from "./chunks/replay-IhsP2Ab4.js";
1
+ import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-BxZz_lNm.js";
2
+ import { n as ReplayTrackerOptions } from "./chunks/replay-BuX3_0xs.js";
3
3
 
4
4
  //#region src/utils/types.d.ts
5
5
  interface SendDataOptions {
@@ -13,6 +13,9 @@ interface SendDataOptions {
13
13
  keepalive?: boolean;
14
14
  }
15
15
  //#endregion
16
+ //#region src/utils/send-data.d.ts
17
+ declare function sendData(options: SendDataOptions): Promise<boolean>;
18
+ //#endregion
16
19
  //#region src/analytics.d.ts
17
20
  type Dict = Record<string, unknown>;
18
21
  interface SamplingOptions {
@@ -66,7 +69,6 @@ declare function optIn(): void;
66
69
  declare function optOut(): void;
67
70
  declare function reportError(error: Error): void;
68
71
  declare function isTrackingDisabled(): boolean;
69
- declare function sendData(options: SendDataOptions): Promise<boolean>;
70
72
  declare class WebAnalytics {
71
73
  private readonly options;
72
74
  private readonly webEndpoint;
@@ -107,6 +109,7 @@ declare class WebAnalytics {
107
109
  private stopNavigationTimer;
108
110
  private stopChildTrackers;
109
111
  private registerChildTracker;
112
+ private startChildTracker;
110
113
  private startErrorTracker;
111
114
  private startWebVitalsTracker;
112
115
  private startReplayTracker;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- 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"./chunks/analytics-ur06JW5D.js";import{n as d}from"./chunks/feature-flags-CqVtrpX2.js";export{l as WebAnalytics,d as fetchFeatureFlagEvaluation,a as getInstance,s as identify,r as isTrackingDisabled,e as logout,o as optIn,c as optOut,t as reportError,i as sendData,u as setConsentMode,n as trackEvent};
1
+ import{i as e,n as t,o as n,r}from"./chunks/api-urls-BrkcoElX.js";import{a as i,i as a,n as o,o as s,r as c,s as l,t as u}from"./chunks/identifiers-CQeWm7wi.js";import{t as d}from"./chunks/send-data-DL_GlsQw.js";import{t as f}from"./chunks/types-CYzR5xtT.js";import{n as p}from"./chunks/feature-flags-CjnLZGxp.js";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:f(r.samplingPercentage??e.sessionReplays?.sampling?.percentage)}}const g={instance:null,pendingConsentMode:void 0};function _(){return g.instance}function v(e,t){typeof window>`u`||T()||g.instance?.track(e,t??{})}function y(e,t,n){return typeof window>`u`||T()?Promise.resolve(!1):g.instance?.identify(e,t,n??{})??Promise.resolve(!1)}function b(e=!0){typeof window>`u`||T()||g.instance?.logout(e)}function x(e){if(g.instance){g.instance.setConsentMode(e);return}g.pendingConsentMode=e}function S(){x(`granted`)}function C(){x(`denied`)}function w(e){typeof window>`u`||T()||g.instance?.reportError(e)}function T(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}function E(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function D(){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 O=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`?(a(),this.leavePage(),this.stopHeartbeat()):(a(),this.enterPage(),this.startHeartbeat())};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(t){this.options=t,this.baseUrl=r(t.baseUrl),this.featureFlagsBaseUrl=e(t.featureFlagsBaseUrl),this.webEndpoint=n(this.baseUrl),this.debug=t.debug??!1,this.consentMode=t.consent?.mode??`granted`,this.cookielessWhilePending=t.consent?.cookielessWhilePending??!0,g.pendingConsentMode!==void 0&&(this.consentMode=g.pendingConsentMode,g.pendingConsentMode=void 0),(t.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(T()){this.log(`disabled`);return}g.instance=this,l(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||T()?!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.instance!==this?(e.stop?.(),!1):(this.childTrackers.push(e),!0)}async startChildTracker(e,t,n){try{let r=await t();return!this.started||this.destroyed||g.instance!==this?(r.stop?.(),null):(r.start(),this.registerChildTracker(r)?(n?.(r),this.log(`${e} loaded`),r):null)}catch(t){return this.log(`failed to initialize ${e} tracker: ${String(t)}`),null}}async startErrorTracker(){await this.startChildTracker(`error`,async()=>{let{default:e}=await import(`./chunks/error-CttYL43D.js`).then(e=>e.n);return new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug,sdkName:this.options.sdkName,sdkVersion:this.options.sdkVersion})},e=>{for(this.errorTracker=e;this.pendingReportedErrors.length>0;){let t=this.pendingReportedErrors.shift();t&&e.captureError(t)}})}async startWebVitalsTracker(){await this.startChildTracker(`web-vitals`,async()=>{let{default:e}=await import(`./chunks/web-vitals-CjA1bFLG.js`).then(e=>e.n);return new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:f(this.options.webVitals?.sampling?.percentage),attribution:this.options.webVitals?.attribution??!1})})}async startReplayTracker(e){await this.startChildTracker(`replay`,async()=>{let{default:t}=await import(`./chunks/replay-BrMLCiBF.js`).then(e=>e.n);return new t(e)})}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(g.instance&&g.instance!==this){this.log(`already started by another instance`);return}if(T()){this.log(`disabled`);return}this.started=!0,g.instance=this,l(this.isCookielessMode()),o();let e=this.options,t=h(e,this.baseUrl,this.debug);t&&this.startReplayTracker(t),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.instance===this&&(g.instance=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,n,r={}){if(!this.ensureStarted()||this.isCookielessMode())return Promise.resolve(!1);let i=e.trim(),a=n.trim();return!i||!a?Promise.resolve(!1):d({url:t(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:u(!1),externalId:i,email:a,name:r.name?.trim()||void 0,phone:r.phone?.trim()||void 0,avatarUrl:r.avatarUrl?.trim()||void 0,traits:r.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`,useBeacon:!1})}logout(e=!0){this.ensureStarted()&&(e&&i(this.isCookielessMode()),s())}setConsentMode(e){this.consentMode=e,l(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return u(this.isCookielessMode())}getSessionId(){return o()}async checkFeatureFlag(e,t,n){if(typeof window>`u`||T())return{value:`false`};let r=n?.externalId?.trim(),i=this.getAnonymousId();if(!r&&!i)return{value:`false`};let{fetchFeatureFlagEvaluation:a}=await import(`./chunks/feature-flags-CjnLZGxp.js`).then(e=>e.t);return a(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,...i?{identifier:i}:{},...r?{externalId:r}:{},attributes:t,signal:n?.signal})}reportError(e){if(this.destroyed||typeof window>`u`||T()||!(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||T())return;let n=u(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:o(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...D(),...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-c()})}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}a()},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;if(!(!e&&!t)){this.leavePage(),this.enterPage();for(let e of this.childTrackers)e.trackPageChange?.(this.pageUrl);this.trackScroll(),this.pageview({trigger:`navigation`})}},300))}links(){let e=e=>{let t=E(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{O as WebAnalytics,p as fetchFeatureFlagEvaluation,_ as getInstance,y as identify,T as isTrackingDisabled,b as logout,S as optIn,C as optOut,w as reportError,d as sendData,x as setConsentMode,v as trackEvent};
package/dist/replay.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- import { n as ReplayTrackerOptions, t as ReplayTracker } from "./chunks/replay-IhsP2Ab4.js";
1
+ import { n as ReplayTrackerOptions, t as ReplayTracker } from "./chunks/replay-BuX3_0xs.js";
2
2
  export { type ReplayTrackerOptions, ReplayTracker as default };
package/dist/replay.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./chunks/replay-DQHUrcod.js";export{e as default};
1
+ import{t as e}from"./chunks/replay-BrMLCiBF.js";export{e as default};
@@ -9,19 +9,29 @@ interface WebVitalsOptions {
9
9
  declare class WebVitalsTracker {
10
10
  private readonly options;
11
11
  private readonly endpoint;
12
- private readonly metrics;
12
+ private readonly metricsByUrl;
13
+ private readonly pendingFlushUrls;
13
14
  private readonly sampled;
14
15
  private started;
15
- private flushed;
16
+ private flushing;
17
+ private finalFlushRequested;
18
+ private currentUrl;
16
19
  constructor(options: WebVitalsOptions);
17
20
  private get debug();
18
21
  private log;
19
22
  start(): void;
20
23
  stop(): void;
24
+ trackPageChange(url?: string): void;
25
+ private cleanupListeners;
21
26
  private observe;
22
27
  private onVisibilityChange;
28
+ private onPageHide;
23
29
  private captureMetric;
24
- private flush;
30
+ private metricsForUrl;
31
+ private queueFlush;
32
+ private flushPending;
33
+ private sendMetricsForUrl;
34
+ private finishFinalFlushIfNeeded;
25
35
  }
26
36
  //#endregion
27
37
  export { type WebVitalsOptions, WebVitalsTracker as default };
@@ -1 +1 @@
1
- import{t as e}from"./chunks/web-vitals-BooBPWJC.js";export{e as default};
1
+ import{t as e}from"./chunks/web-vitals-CjA1bFLG.js";export{e as default};
package/package.json CHANGED
@@ -39,7 +39,7 @@
39
39
  "publishConfig": {
40
40
  "access": "public"
41
41
  },
42
- "version": "0.2.10",
42
+ "version": "0.2.12",
43
43
  "scripts": {
44
44
  "build": "tsdown && bun run check-size",
45
45
  "dev": "bun run build",
package/src/analytics.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type ErrorTracker from "./error";
2
2
  import type { FeatureFlagEvaluation } from "./feature-flags";
3
- import type ReplayTracker from "./replay";
4
3
  import type { ReplayTrackerOptions } from "./replay";
5
4
  import {
6
5
  identifyEventsUrl,
@@ -17,18 +16,21 @@ import {
17
16
  resetSessionId,
18
17
  setCookielessMode,
19
18
  } from "./utils/identifiers";
19
+ import { sendData } from "./utils/send-data";
20
20
  import {
21
21
  normalizeSamplingPercentage,
22
22
  type SendDataOptions,
23
23
  } from "./utils/types";
24
- import type WebVitalsTracker from "./web-vitals";
25
24
 
26
25
  export type { SendDataOptions };
26
+ export { sendData };
27
27
 
28
28
  type ChildTracker = {
29
29
  start(): void;
30
30
  stop?(): void;
31
+ trackPageChange?(url?: string): void;
31
32
  };
33
+ type TrackerFactory<T extends ChildTracker> = () => Promise<T>;
32
34
 
33
35
  type Dict = Record<string, unknown>;
34
36
 
@@ -180,73 +182,6 @@ export function isTrackingDisabled(): boolean {
180
182
  return value === "true" || value === "1";
181
183
  }
182
184
 
183
- export async function sendData(options: SendDataOptions): Promise<boolean> {
184
- const {
185
- url,
186
- data,
187
- contentType = "application/json",
188
- headers = {},
189
- debug = false,
190
- debugPrefix = "[Analytics]",
191
- useBeacon = true,
192
- keepalive = true,
193
- } = options;
194
-
195
- const canUseBeacon =
196
- useBeacon &&
197
- typeof navigator !== "undefined" &&
198
- typeof navigator.sendBeacon === "function";
199
- if (canUseBeacon) {
200
- try {
201
- const beaconBody =
202
- data instanceof Blob
203
- ? data
204
- : typeof Blob !== "undefined"
205
- ? new Blob([data as BlobPart], { type: contentType })
206
- : data;
207
- if (navigator.sendBeacon(url, beaconBody as BodyInit)) {
208
- if (debug) {
209
- console.log(`${debugPrefix} Sent via beacon`);
210
- }
211
- return true;
212
- }
213
- } catch {}
214
- }
215
-
216
- if (typeof fetch !== "function") {
217
- if (debug) {
218
- console.warn(`${debugPrefix} Failed to send`);
219
- }
220
- return false;
221
- }
222
-
223
- try {
224
- const response = await fetch(url, {
225
- method: "POST",
226
- body: data as BodyInit,
227
- headers: {
228
- "Content-Type": contentType,
229
- ...headers,
230
- },
231
- keepalive,
232
- });
233
- const success = response.ok;
234
- if (debug) {
235
- if (success) {
236
- console.log(`${debugPrefix} Sent via fetch`);
237
- } else {
238
- console.warn(`${debugPrefix} Failed: ${response.status}`);
239
- }
240
- }
241
- return success;
242
- } catch {
243
- if (debug) {
244
- console.warn(`${debugPrefix} Failed to send`);
245
- }
246
- return false;
247
- }
248
- }
249
-
250
185
  function getLinkEl(el: Node | null): HTMLAnchorElement | null {
251
186
  while (el) {
252
187
  if (el instanceof HTMLAnchorElement && el.href) return el;
@@ -297,9 +232,12 @@ export class WebAnalytics {
297
232
  private errorTracker: ErrorTracker | null = null;
298
233
  private readonly handleVisibilityChange = (): void => {
299
234
  if (document.visibilityState === "hidden") {
235
+ refreshSessionTimestamp();
300
236
  this.leavePage();
301
237
  this.stopHeartbeat();
302
238
  } else {
239
+ refreshSessionTimestamp();
240
+ this.enterPage();
303
241
  this.startHeartbeat();
304
242
  }
305
243
  };
@@ -429,40 +367,58 @@ export class WebAnalytics {
429
367
  return true;
430
368
  }
431
369
 
432
- private async startErrorTracker(): Promise<void> {
370
+ private async startChildTracker<T extends ChildTracker>(
371
+ name: string,
372
+ createTracker: TrackerFactory<T>,
373
+ onStarted?: (tracker: T) => void,
374
+ ): Promise<T | null> {
433
375
  try {
434
- const { default: ErrorTrackerClass } = await import("./error");
435
- if (!this.started || this.destroyed || moduleState.instance !== this)
436
- return;
376
+ const tracker = await createTracker();
377
+ if (!this.started || this.destroyed || moduleState.instance !== this) {
378
+ tracker.stop?.();
379
+ return null;
380
+ }
437
381
 
438
- const errorTracker: ErrorTracker = new ErrorTrackerClass({
439
- siteKey: this.options.siteKey,
440
- baseUrl: this.baseUrl,
441
- debug: this.debug,
442
- sdkName: this.options.sdkName,
443
- sdkVersion: this.options.sdkVersion,
444
- });
445
- errorTracker.start();
446
- if (!this.registerChildTracker(errorTracker)) return;
382
+ tracker.start();
383
+ if (!this.registerChildTracker(tracker)) return null;
447
384
 
448
- this.errorTracker = errorTracker;
449
- while (this.pendingReportedErrors.length > 0) {
450
- const pending = this.pendingReportedErrors.shift();
451
- if (pending) errorTracker.captureError(pending);
452
- }
453
- this.log("error loaded");
385
+ onStarted?.(tracker);
386
+ this.log(`${name} loaded`);
387
+ return tracker;
454
388
  } catch (error) {
455
- this.log(`failed to initialize error tracker: ${String(error)}`);
389
+ this.log(`failed to initialize ${name} tracker: ${String(error)}`);
390
+ return null;
456
391
  }
457
392
  }
458
393
 
394
+ private async startErrorTracker(): Promise<void> {
395
+ await this.startChildTracker(
396
+ "error",
397
+ async () => {
398
+ const { default: ErrorTrackerClass } = await import("./error");
399
+ return new ErrorTrackerClass({
400
+ siteKey: this.options.siteKey,
401
+ baseUrl: this.baseUrl,
402
+ debug: this.debug,
403
+ sdkName: this.options.sdkName,
404
+ sdkVersion: this.options.sdkVersion,
405
+ });
406
+ },
407
+ (errorTracker) => {
408
+ this.errorTracker = errorTracker;
409
+
410
+ while (this.pendingReportedErrors.length > 0) {
411
+ const pending = this.pendingReportedErrors.shift();
412
+ if (pending) errorTracker.captureError(pending);
413
+ }
414
+ },
415
+ );
416
+ }
417
+
459
418
  private async startWebVitalsTracker(): Promise<void> {
460
- try {
419
+ await this.startChildTracker("web-vitals", async () => {
461
420
  const { default: WebVitalsTrackerClass } = await import("./web-vitals");
462
- if (!this.started || this.destroyed || moduleState.instance !== this)
463
- return;
464
-
465
- const webVitalsTracker: WebVitalsTracker = new WebVitalsTrackerClass({
421
+ return new WebVitalsTrackerClass({
466
422
  siteKey: this.options.siteKey,
467
423
  baseUrl: this.baseUrl,
468
424
  debug: this.debug,
@@ -471,33 +427,16 @@ export class WebAnalytics {
471
427
  ),
472
428
  attribution: this.options.webVitals?.attribution ?? false,
473
429
  });
474
- webVitalsTracker.start();
475
- if (!this.registerChildTracker(webVitalsTracker)) return;
476
-
477
- this.log("web-vitals loaded");
478
- } catch (error) {
479
- this.log(`failed to initialize web-vitals tracker: ${String(error)}`);
480
- }
430
+ });
481
431
  }
482
432
 
483
433
  private async startReplayTracker(
484
434
  replayOptions: ReplayTrackerOptions,
485
435
  ): Promise<void> {
486
- try {
436
+ await this.startChildTracker("replay", async () => {
487
437
  const { default: ReplayTrackerClass } = await import("./replay");
488
- if (!this.started || this.destroyed || moduleState.instance !== this)
489
- return;
490
-
491
- const replayTracker: ReplayTracker = new ReplayTrackerClass(
492
- replayOptions,
493
- );
494
- replayTracker.start();
495
- if (!this.registerChildTracker(replayTracker)) return;
496
-
497
- this.log("replay loaded");
498
- } catch (error) {
499
- this.log(`failed to initialize replay tracker: ${String(error)}`);
500
- }
438
+ return new ReplayTrackerClass(replayOptions);
439
+ });
501
440
  }
502
441
 
503
442
  async start(): Promise<void> {
@@ -816,6 +755,9 @@ export class WebAnalytics {
816
755
  if (!pathChanged && !hashChanged) return;
817
756
  this.leavePage();
818
757
  this.enterPage();
758
+ for (const tracker of this.childTrackers) {
759
+ tracker.trackPageChange?.(this.pageUrl);
760
+ }
819
761
  this.trackScroll();
820
762
  this.pageview({ trigger: "navigation" });
821
763
  }, 300);
package/src/error.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { sendData } from "./analytics";
2
1
  import { SDK_NAME, SDK_VERSION } from "./sdk";
3
2
  import { webEventsUrl } from "./utils/api-urls";
4
3
  import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
4
+ import { sendData } from "./utils/send-data";
5
5
 
6
6
  export type ErrorEntry = {
7
7
  error: string;
@@ -98,6 +98,7 @@ export default class ErrorTracker {
98
98
  private readonly queue = new Map<string, ErrorTracking>();
99
99
  private timer: ReturnType<typeof setInterval> | null = null;
100
100
  private started = false;
101
+ private flushing = false;
101
102
 
102
103
  constructor(private readonly options: ErrorTrackingOptions) {
103
104
  this.endpoint = webEventsUrl(options.baseUrl);
@@ -126,9 +127,9 @@ export default class ErrorTracker {
126
127
  window.addEventListener("error", this.onError);
127
128
  window.addEventListener("unhandledrejection", this.onRejection);
128
129
  document.addEventListener("visibilitychange", this.onVisibilityChange);
129
- window.addEventListener("pagehide", this.flush);
130
+ window.addEventListener("pagehide", this.requestFlush);
130
131
 
131
- this.timer = setInterval(this.flush, this.flushInterval);
132
+ this.timer = setInterval(this.requestFlush, this.flushInterval);
132
133
  this.log("Started listening for errors");
133
134
  }
134
135
 
@@ -139,12 +140,12 @@ export default class ErrorTracker {
139
140
  window.removeEventListener("error", this.onError);
140
141
  window.removeEventListener("unhandledrejection", this.onRejection);
141
142
  document.removeEventListener("visibilitychange", this.onVisibilityChange);
142
- window.removeEventListener("pagehide", this.flush);
143
+ window.removeEventListener("pagehide", this.requestFlush);
143
144
 
144
145
  if (this.timer) clearInterval(this.timer);
145
146
  this.timer = null;
146
147
 
147
- this.flush();
148
+ this.requestFlush();
148
149
  this.log("Stopped listening for errors");
149
150
  }
150
151
 
@@ -194,9 +195,22 @@ export default class ErrorTracker {
194
195
  };
195
196
 
196
197
  private onVisibilityChange = (): void => {
197
- if (document.visibilityState === "hidden") this.flush();
198
+ if (document.visibilityState === "hidden") this.requestFlush();
198
199
  };
199
200
 
201
+ private requestFlush = (): void => {
202
+ void this.flush();
203
+ };
204
+
205
+ private restoreErrors(errors: ErrorTracking[]): void {
206
+ for (const error of errors) {
207
+ this.queue.set(error.hash, {
208
+ ...error,
209
+ count: error.count + (this.queue.get(error.hash)?.count ?? 0),
210
+ });
211
+ }
212
+ }
213
+
200
214
  private record(data: NormalizedError, original?: unknown): void {
201
215
  if (original instanceof Error) {
202
216
  if (this.handled.has(original)) {
@@ -231,15 +245,16 @@ export default class ErrorTracker {
231
245
  this.log("Captured error:", data);
232
246
 
233
247
  if (this.queue.size >= this.maxQueueSize) {
234
- this.flush();
248
+ this.requestFlush();
235
249
  }
236
250
  }
237
251
 
238
- private flush = (): void => {
239
- if (this.queue.size === 0) return;
252
+ private async flush(): Promise<void> {
253
+ if (this.flushing || this.queue.size === 0) return;
240
254
 
241
255
  const errors = [...this.queue.values()];
242
256
  this.queue.clear();
257
+ this.flushing = true;
243
258
 
244
259
  const identifier = getAnonymousId();
245
260
  const sourcemapsBuild = (
@@ -273,11 +288,18 @@ export default class ErrorTracker {
273
288
  this.log("Flushing errors:", errors);
274
289
  this.log("Payload:", payload);
275
290
 
276
- sendData({
277
- url: this.endpoint,
278
- data: payload,
279
- debug: this.debug,
280
- debugPrefix: "[ErrorTracker]",
281
- });
282
- };
291
+ try {
292
+ const sent = await sendData({
293
+ url: this.endpoint,
294
+ data: payload,
295
+ debug: this.debug,
296
+ debugPrefix: "[ErrorTracker]",
297
+ });
298
+ if (!sent) this.restoreErrors(errors);
299
+ } catch {
300
+ this.restoreErrors(errors);
301
+ } finally {
302
+ this.flushing = false;
303
+ }
304
+ }
283
305
  }
package/src/replay.ts CHANGED
@@ -6,13 +6,13 @@ import {
6
6
  import { record } from "rrweb";
7
7
  import type { recordOptions } from "rrweb/typings/types";
8
8
  import type { SlimDOMOptions } from "rrweb-snapshot";
9
- import { sendData } from "./analytics";
10
9
  import { replayEventsUrl } from "./utils/api-urls";
11
10
  import {
12
11
  getAnonymousId,
13
12
  getOrCreateSessionId,
14
13
  getSessionStart,
15
14
  } from "./utils/identifiers";
15
+ import { sendData } from "./utils/send-data";
16
16
  import { normalizeSamplingPercentage } from "./utils/types";
17
17
 
18
18
  const RRWEB_SEQUENTIAL_ID_KEY = "_faststatsSeqId";
@@ -47,6 +47,7 @@ type ReplayBatch = {
47
47
  token: string;
48
48
  sessionId: string | undefined;
49
49
  identifier?: string;
50
+ batchId: string;
50
51
  sequence: number;
51
52
  timestamp: number;
52
53
  url: string;
@@ -299,18 +300,28 @@ export default class ReplayTracker {
299
300
 
300
301
  private createBatch(events: eventWithTime[]): ReplayBatch {
301
302
  const identifier = getAnonymousId();
303
+ const sequence = this.sequence++;
302
304
 
303
305
  return {
304
306
  token: this.options.siteKey,
305
307
  sessionId: this.getSessionId(),
306
308
  ...(identifier ? { identifier } : {}),
307
- sequence: this.sequence++,
309
+ batchId: this.createBatchId(sequence),
310
+ sequence,
308
311
  timestamp: Date.now(),
309
312
  url: window.location.href,
310
313
  events,
311
314
  };
312
315
  }
313
316
 
317
+ private createBatchId(sequence: number): string {
318
+ const random =
319
+ typeof crypto !== "undefined" && "randomUUID" in crypto
320
+ ? crypto.randomUUID()
321
+ : `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;
322
+ return `${this.getSessionId() ?? "unknown"}-${sequence}-${random}`;
323
+ }
324
+
314
325
  private getBatchSizeBytes(batch: ReplayBatch): number {
315
326
  return new TextEncoder().encode(JSON.stringify(batch)).byteLength;
316
327
  }