@faststats/web 0.2.12 → 0.2.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # @faststats/web
2
2
 
3
+ ## 0.2.13
4
+
5
+ ### Patch Changes
6
+
7
+ - 28fb1ce: chore: provide session_id to flag validation service
8
+
3
9
  ## 0.2.12
4
10
 
5
11
  ### Patch Changes
@@ -0,0 +1 @@
1
+ const e=`https://metrics.faststats.dev`,t=`https://flags.faststats.dev`;function n(e){return e.replace(/\/+$/,``)}function r(t){return t===void 0||t===``?e:n(t)||e}const i={events:`/v1/web`,identify:`/v1/identify`,replay:`/v1/replay`,vitals:`/v1/vitals`,flags:`/v1/check`};function a(e){return e===void 0||e===``?t:n(e)||t}export{r as n,a as r,i as t};
@@ -0,0 +1,2 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{n as t,t as n}from"./api-urls-DaeYkG0_.js";import{n as r,t as i}from"./identifiers-CQeWm7wi.js";import{t as a}from"./send-data-DL_GlsQw.js";var o=e({default:()=>p});const s=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//,c=e=>e?.split(`
2
+ `).map(e=>e.trim()).filter(Boolean);function l(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:c(e.stack),cause:l(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function u(e){let t=``;for(let n=e;n;n=n.cause)t+=`${n.error}\0${n.message??``}\0`;return t}function d(e){let t=2166136261,n=3598710387;for(let r=0;r<e.length;r++){let i=e.charCodeAt(r);t=Math.imul(t^i,16777619),n=Math.imul(n^i,2246822519)}return`err_${(t>>>0).toString(16).padStart(8,`0`)}${(n>>>0).toString(16).padStart(8,`0`)}`}function f(){let e=globalThis.__SOURCEMAPS_BUILD__?.buildId;return typeof e==`string`&&e.trim()?e:void 0}var p=class{endpoint;seen=new WeakSet;queue=new Map;timer=null;started=!1;flushing=!1;constructor(e){this.options=e,this.endpoint=`${t(e.baseUrl)}${n.events}`}log(...e){this.options.debug&&console.log(`[ErrorTracker]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.onError),window.addEventListener(`unhandledrejection`,this.onRejection),window.addEventListener(`pagehide`,this.scheduleFlush),document.addEventListener(`visibilitychange`,this.onVisibility),this.timer=setInterval(this.scheduleFlush,this.options.flushInterval??5e3),this.log(`Started`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.onError),window.removeEventListener(`unhandledrejection`,this.onRejection),window.removeEventListener(`pagehide`,this.scheduleFlush),document.removeEventListener(`visibilitychange`,this.onVisibility),this.timer&&clearInterval(this.timer),this.timer=null,this.scheduleFlush(),this.log(`Stopped`))}captureError(e){this.capture(`error`,e.message,!0,e)}onError=e=>{this.capture(`error`,e.message||`Unknown error`,!1,e.error,e.filename||``,e.lineno)};onRejection=e=>{let t=e.reason,n=t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`;this.capture(`unhandledrejection`,n,!1,t)};onVisibility=()=>{document.visibilityState===`hidden`&&this.scheduleFlush()};scheduleFlush=()=>{this.flush()};capture(e,t,n,r,i=``,a=``){let o=r instanceof Error?r.stack??``:``;if(s.test(`${i}\n${o}`))return;if(typeof r==`object`&&r){if(this.seen.has(r)){this.log(`Skipping duplicate:`,t);return}this.seen.add(r)}let f={error:e===`unhandledrejection`?`UnhandledRejection`:`Error`,message:t,handled:n};if(r instanceof Error){f.stack=c(r.stack);let e=l(r.cause);e&&(f.cause=e)}let p=d(`${e}\0${n?`handled`:`unhandled`}\0${t}\0${i}\0${a}\0${u(f.cause)}`),m=this.queue.get(p);m?m.count++:this.queue.set(p,{hash:p,count:1,...f}),this.log(`Captured:`,f),this.queue.size>=(this.options.maxQueueSize??50)&&this.scheduleFlush()}async flush(){if(this.flushing||this.queue.size===0)return;let e=[...this.queue.values()];this.queue.clear(),this.flushing=!0;let t=i(),n=f(),o=JSON.stringify({token:this.options.siteKey,...t?{userId:t}:{},sessionId:r(),...n?{buildId:n}:{},sdkName:this.options.sdkName??`@faststats/web`,sdkVersion:this.options.sdkVersion??`0.2.13`,data:{url:location.href,page:location.pathname,referrer:document.referrer||null,title:document.title},errors:e});this.log(`Flushing:`,e);try{await a({url:this.endpoint,data:o,debug:this.options.debug,debugPrefix:`[ErrorTracker]`})||this.requeue(e)}catch{this.requeue(e)}finally{this.flushing=!1}}requeue(e){for(let t of e){let e=this.queue.get(t.hash);e?e.count+=t.count:this.queue.set(t.hash,t)}}};export{o as n,p as t};
@@ -10,6 +10,7 @@ type FeatureFlagCheckContext = {
10
10
  projectId?: string;
11
11
  identifier?: string;
12
12
  externalId?: string;
13
+ sessionId?: string;
13
14
  attributes?: Record<string, unknown>;
14
15
  signal?: AbortSignal;
15
16
  };
@@ -0,0 +1 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{r as t,t as n}from"./api-urls-DaeYkG0_.js";var r=e({fetchFeatureFlagEvaluation:()=>i});async function i(e,r){if(r.projectToken&&r.projectId)throw Error(`provide either projectToken or projectId, not both`);let i=r.projectToken,a=r.projectId,o=r.identifier,s=r.externalId,c=r.sessionId;if(!i&&!a)throw Error(`feature flag check requires projectToken or projectId`);if(!o&&!s)throw Error(`feature flag check requires serverId or externalId`);let l={key:e,...a?{projectId:a}:{},...o?{identifier:o}:{},...s?{externalId:s}:{},...c?{sessionId:c}:{}};r.attributes&&Object.keys(r.attributes).length>0&&(l.attributes=r.attributes);let u={"Content-Type":`application/json`};i&&(u.Authorization=`Bearer ${i}`);let d=`${t(r.baseUrl)}${n.flags}`,f=await fetch(d,{method:`POST`,headers:u,body:JSON.stringify(l),credentials:`omit`,signal:r.signal});if(!f.ok){let e=``;try{let t=await f.json();typeof t.error==`string`&&t.error.length>0&&(e=` — ${t.error}`)}catch{}throw Error(`feature flag check failed: HTTP ${f.status}${e}`)}return await f.json()}export{i as n,r as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{n as t,t as n}from"./api-urls-DaeYkG0_.js";import{n as r,r as i,t as a}from"./identifiers-CQeWm7wi.js";import{t as o}from"./send-data-DL_GlsQw.js";import{t as s}from"./types-CYzR5xtT.js";import{EventType as c}from"@rrweb/types";import{record as l}from"rrweb";var u=e({default:()=>p});const d={mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},f={script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0};var p=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)}${n.replay}`,this.sampled=Math.random()*100<s(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=r(),this.startTime=i(),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=l({emit:this.onEvent,sampling:this.options.sampling??d,slimDOMOptions:this.options.slimDOMOptions??f,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??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 t=a(),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 o({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{u as n,p as t};
@@ -0,0 +1 @@
1
+ import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{n as t,t as n}from"./api-urls-DaeYkG0_.js";import{n as r}from"./identifiers-CQeWm7wi.js";import{t as i}from"./types-CYzR5xtT.js";var a=e({default:()=>o}),o=class{endpoint;sampled;metrics=new Map;started=!1;url=``;constructor(e){this.options=e,this.endpoint=`${t(e.baseUrl)}${n.vitals}`,this.sampled=Math.random()*100<i(e.samplingPercentage)}get debug(){return this.options.debug??!1}log(...e){this.debug&&console.log(`[WebVitals]`,...e)}async start(){if(this.started||typeof window>`u`)return;this.started=!0,this.url=window.location.href,document.addEventListener(`visibilitychange`,this.onHidden,{passive:!0}),window.addEventListener(`pagehide`,this.onPageHide,{passive:!0});let e=this.options.attribution?await import(`web-vitals/attribution`):await import(`web-vitals`);this.started&&(e.onCLS(this.capture,{reportAllChanges:!0}),e.onINP(this.capture,{reportAllChanges:!0}),e.onLCP(this.capture,{reportAllChanges:!0}),e.onFCP(this.capture),e.onTTFB(this.capture),this.log(`Tracking started`))}stop(){!this.started||typeof window>`u`||(this.started=!1,this.removeListeners(),this.flush())}trackPageChange(e){if(!this.started||typeof window>`u`)return;let t=e??window.location.href;t!==this.url&&(this.flush(),this.url=t)}onHidden=()=>{document.visibilityState===`hidden`&&this.flush()};onPageHide=()=>{this.flush()};capture=e=>{if(!this.started||!this.sampled)return;let t=e.name,n=e.attribution;this.metrics.set(t,{metric:t,value:e.value,attributes:{id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...n??{}}})};async flush(){if(typeof window>`u`||typeof fetch!=`function`||!this.metrics.size)return;let e=Array.from(this.metrics.values()),t=this.url;this.metrics.clear();try{let n=new AbortController,i=window.setTimeout(()=>n.abort(),3e3),a=await fetch(this.endpoint,{method:`POST`,body:JSON.stringify({sessionId:r(),vitals:e,metadata:{url:t}}),headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.options.siteKey}`},keepalive:!0,signal:n.signal});if(clearTimeout(i),!a.ok)throw Error(`HTTP ${a.status}`)}catch(e){this.log(`Failed to send metrics`,e)}}removeListeners(){document.removeEventListener(`visibilitychange`,this.onHidden),window.removeEventListener(`pagehide`,this.onPageHide)}};export{a as n,o as t};
package/dist/error.d.ts CHANGED
@@ -22,26 +22,23 @@ interface ErrorTrackingOptions {
22
22
  declare class ErrorTracker {
23
23
  private readonly options;
24
24
  private readonly endpoint;
25
- private readonly handled;
25
+ private readonly seen;
26
26
  private readonly queue;
27
27
  private timer;
28
28
  private started;
29
29
  private flushing;
30
30
  constructor(options: ErrorTrackingOptions);
31
- private get debug();
32
- private get flushInterval();
33
- private get maxQueueSize();
34
31
  private log;
35
32
  start(): void;
36
33
  stop(): void;
37
34
  captureError(error: Error): void;
38
35
  private onError;
39
36
  private onRejection;
40
- private onVisibilityChange;
41
- private requestFlush;
42
- private restoreErrors;
43
- private record;
37
+ private onVisibility;
38
+ private scheduleFlush;
39
+ private capture;
44
40
  private flush;
41
+ private requeue;
45
42
  }
46
43
  //#endregion
47
44
  export { type ErrorEntry, type ErrorTracking, type ErrorTrackingOptions, ErrorTracker as default };
package/dist/error.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./chunks/error-CttYL43D.js";export{e as default};
1
+ import{t as e}from"./chunks/error-CDVOMiI9.js";export{e as default};
@@ -1,2 +1,2 @@
1
- import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-BxZz_lNm.js";
1
+ import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-BClx56v5.js";
2
2
  export { type FeatureFlagCheckContext, type FeatureFlagEvaluation, fetchFeatureFlagEvaluation };
@@ -1 +1 @@
1
- import{n as e}from"./chunks/feature-flags-CjnLZGxp.js";export{e as fetchFeatureFlagEvaluation};
1
+ import{n as e}from"./chunks/feature-flags-DSOCIZHK.js";export{e as fetchFeatureFlagEvaluation};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-BxZz_lNm.js";
1
+ import { n as FeatureFlagEvaluation, r as fetchFeatureFlagEvaluation, t as FeatureFlagCheckContext } from "./chunks/feature-flags-BClx56v5.js";
2
2
  import { n as ReplayTrackerOptions } from "./chunks/replay-BuX3_0xs.js";
3
3
 
4
4
  //#region src/utils/types.d.ts
package/dist/index.js CHANGED
@@ -1 +1 @@
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};
1
+ import{n as e,r as t,t as n}from"./chunks/api-urls-DaeYkG0_.js";import{a as r,i,n as a,o,r as s,s as c,t as l}from"./chunks/identifiers-CQeWm7wi.js";import{t as u}from"./chunks/send-data-DL_GlsQw.js";import{t as d}from"./chunks/types-CYzR5xtT.js";import{n as f}from"./chunks/feature-flags-DSOCIZHK.js";function p(e){return e.replayOptions?.samplingPercentage!==void 0||e.sessionReplays?.sampling?.percentage!==void 0}function m(e,t,n){if(!(e.sessionReplays?.enabled??e.trackReplay??p(e)))return null;let r=e.replayOptions??{};return{siteKey:e.siteKey,baseUrl:t,debug:n,...r,samplingPercentage:d(r.samplingPercentage??e.sessionReplays?.sampling?.percentage)}}const h={instance:null,pendingConsentMode:void 0};function g(){return h.instance}function _(e,t){typeof window>`u`||w()||h.instance?.track(e,t??{})}function v(e,t,n){return typeof window>`u`||w()?Promise.resolve(!1):h.instance?.identify(e,t,n??{})??Promise.resolve(!1)}function y(e=!0){typeof window>`u`||w()||h.instance?.logout(e)}function b(e){if(h.instance){h.instance.setConsentMode(e);return}h.pendingConsentMode=e}function x(){b(`granted`)}function S(){b(`denied`)}function C(e){typeof window>`u`||w()||h.instance?.reportError(e)}function w(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}function T(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function E(){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 D=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`?(i(),this.leavePage(),this.stopHeartbeat()):(i(),this.enterPage(),this.startHeartbeat())};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(r){this.options=r,this.baseUrl=e(r.baseUrl),this.featureFlagsBaseUrl=t(r.featureFlagsBaseUrl),this.webEndpoint=`${this.baseUrl}${n.events}`,this.debug=r.debug??!1,this.consentMode=r.consent?.mode??`granted`,this.cookielessWhilePending=r.consent?.cookielessWhilePending??!0,h.pendingConsentMode!==void 0&&(this.consentMode=h.pendingConsentMode,h.pendingConsentMode=void 0),(r.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(w()){this.log(`disabled`);return}h.instance=this,c(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||w()?!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||h.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||h.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-CDVOMiI9.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-GwPlL7Zy.js`).then(e=>e.n);return new e({siteKey:this.options.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:d(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-CHFW2q4E.js`).then(e=>e.n);return new t(e)})}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(h.instance&&h.instance!==this){this.log(`already started by another instance`);return}if(w()){this.log(`disabled`);return}this.started=!0,h.instance=this,c(this.isCookielessMode()),a();let e=this.options,t=m(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(),h.instance===this&&(h.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,t,r={}){if(!this.ensureStarted()||this.isCookielessMode())return Promise.resolve(!1);let i=e.trim(),a=t.trim();return!i||!a?Promise.resolve(!1):u({url:`${this.baseUrl}${n.identify}`,data:JSON.stringify({token:this.options.siteKey,identifier:l(!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&&r(this.isCookielessMode()),o())}setConsentMode(e){this.consentMode=e,c(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return l(this.isCookielessMode())}getSessionId(){return a()}async checkFeatureFlag(e,t,n){if(typeof window>`u`||w())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-DSOCIZHK.js`).then(e=>e.t);return a(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,...i?{identifier:i}:{},...r?{externalId:r}:{},sessionId:this.getSessionId(),attributes:t,signal:n?.signal})}reportError(e){if(this.destroyed||typeof window>`u`||w()||!(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||w())return;let n=l(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:a(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...E(),...t}});this.log(e),u({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-s()})}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}i()},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=T(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{D as WebAnalytics,f as fetchFeatureFlagEvaluation,g as getInstance,v as identify,w as isTrackingDisabled,y as logout,x as optIn,S as optOut,C as reportError,u as sendData,b as setConsentMode,_ as trackEvent};
package/dist/replay.js CHANGED
@@ -1 +1 @@
1
- import{t as e}from"./chunks/replay-BrMLCiBF.js";export{e as default};
1
+ import{t as e}from"./chunks/replay-CHFW2q4E.js";export{e as default};
@@ -9,29 +9,21 @@ interface WebVitalsOptions {
9
9
  declare class WebVitalsTracker {
10
10
  private readonly options;
11
11
  private readonly endpoint;
12
- private readonly metricsByUrl;
13
- private readonly pendingFlushUrls;
14
12
  private readonly sampled;
13
+ private readonly metrics;
15
14
  private started;
16
- private flushing;
17
- private finalFlushRequested;
18
- private currentUrl;
15
+ private url;
19
16
  constructor(options: WebVitalsOptions);
20
17
  private get debug();
21
18
  private log;
22
- start(): void;
19
+ start(): Promise<void>;
23
20
  stop(): void;
24
21
  trackPageChange(url?: string): void;
25
- private cleanupListeners;
26
- private observe;
27
- private onVisibilityChange;
22
+ private onHidden;
28
23
  private onPageHide;
29
- private captureMetric;
30
- private metricsForUrl;
31
- private queueFlush;
32
- private flushPending;
33
- private sendMetricsForUrl;
34
- private finishFinalFlushIfNeeded;
24
+ private capture;
25
+ private flush;
26
+ private removeListeners;
35
27
  }
36
28
  //#endregion
37
29
  export { type WebVitalsOptions, WebVitalsTracker as default };
@@ -1 +1 @@
1
- import{t as e}from"./chunks/web-vitals-CjA1bFLG.js";export{e as default};
1
+ import{t as e}from"./chunks/web-vitals-GwPlL7Zy.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.12",
42
+ "version": "0.2.13",
43
43
  "scripts": {
44
44
  "build": "tsdown && bun run check-size",
45
45
  "dev": "bun run build",
package/src/analytics.ts CHANGED
@@ -2,10 +2,9 @@ import type ErrorTracker from "./error";
2
2
  import type { FeatureFlagEvaluation } from "./feature-flags";
3
3
  import type { ReplayTrackerOptions } from "./replay";
4
4
  import {
5
- identifyEventsUrl,
6
5
  normalizeAnalyticsBaseUrl,
7
6
  normalizeFeatureFlagsBaseUrl,
8
- webEventsUrl,
7
+ URLS,
9
8
  } from "./utils/api-urls";
10
9
  import {
11
10
  getAnonymousId,
@@ -256,7 +255,7 @@ export class WebAnalytics {
256
255
  this.featureFlagsBaseUrl = normalizeFeatureFlagsBaseUrl(
257
256
  options.featureFlagsBaseUrl,
258
257
  );
259
- this.webEndpoint = webEventsUrl(this.baseUrl);
258
+ this.webEndpoint = `${this.baseUrl}${URLS.events}`;
260
259
  this.debug = options.debug ?? false;
261
260
  this.consentMode = options.consent?.mode ?? "granted";
262
261
  this.cookielessWhilePending =
@@ -540,7 +539,7 @@ export class WebAnalytics {
540
539
  const trimmedEmail = email.trim();
541
540
  if (!trimmedExternalId || !trimmedEmail) return Promise.resolve(false);
542
541
 
543
- const identifyEndpoint = identifyEventsUrl(this.baseUrl);
542
+ const identifyEndpoint = `${this.baseUrl}${URLS.identify}`;
544
543
  const payload = JSON.stringify({
545
544
  token: this.options.siteKey,
546
545
  identifier: getAnonymousId(false),
@@ -615,6 +614,7 @@ export class WebAnalytics {
615
614
  projectToken: this.options.siteKey,
616
615
  ...(identifier ? { identifier } : {}),
617
616
  ...(externalId ? { externalId } : {}),
617
+ sessionId: this.getSessionId(),
618
618
  attributes,
619
619
  signal: opts?.signal,
620
620
  });
package/src/error.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SDK_NAME, SDK_VERSION } from "./sdk";
2
- import { webEventsUrl } from "./utils/api-urls";
2
+ import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
3
3
  import { getAnonymousId, getOrCreateSessionId } from "./utils/identifiers";
4
4
  import { sendData } from "./utils/send-data";
5
5
 
@@ -16,18 +16,6 @@ export type ErrorTracking = ErrorEntry & {
16
16
  count: number;
17
17
  };
18
18
 
19
- type ErrorKind = "error" | "unhandledrejection";
20
-
21
- type NormalizedError = {
22
- kind: ErrorKind;
23
- message: string;
24
- stack?: string;
25
- filename?: string;
26
- lineno?: number;
27
- handled: boolean;
28
- cause?: ErrorEntry;
29
- };
30
-
31
19
  export interface ErrorTrackingOptions {
32
20
  siteKey: string;
33
21
  baseUrl?: string;
@@ -38,214 +26,176 @@ export interface ErrorTrackingOptions {
38
26
  sdkVersion?: string;
39
27
  }
40
28
 
41
- const EXTENSION_URL =
29
+ type Kind = "error" | "unhandledrejection";
30
+
31
+ const EXTENSION_RE =
42
32
  /(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;
43
33
 
44
34
  const parseStack = (stack?: string): string[] | undefined =>
45
35
  stack
46
36
  ?.split("\n")
47
- .map((line) => line.trim())
37
+ .map((l) => l.trim())
48
38
  .filter(Boolean);
49
39
 
50
- function serializeCause(cause: unknown): ErrorEntry | undefined {
51
- if (cause instanceof Error) {
40
+ function serializeCause(value: unknown): ErrorEntry | undefined {
41
+ if (value instanceof Error) {
52
42
  return {
53
- error: cause.name?.trim() || "Error",
54
- message: cause.message,
55
- stack: parseStack(cause.stack),
56
- cause: serializeCause(cause.cause),
43
+ error: value.name?.trim() || "Error",
44
+ message: value.message,
45
+ stack: parseStack(value.stack),
46
+ cause: serializeCause(value.cause),
57
47
  };
58
48
  }
59
-
60
- if (typeof cause === "string") {
61
- return { error: "Error", message: cause };
62
- }
49
+ if (typeof value === "string") return { error: "Error", message: value };
63
50
  }
64
51
 
65
52
  function fingerprintCause(cause?: ErrorEntry): string {
66
- return cause
67
- ? `${cause.error}\0${cause.message ?? ""}\0${fingerprintCause(cause.cause)}`
68
- : "";
53
+ let s = "";
54
+ for (let c = cause; c; c = c.cause) {
55
+ s += `${c.error}\0${c.message ?? ""}\0`;
56
+ }
57
+ return s;
69
58
  }
70
59
 
71
- function hashError(data: NormalizedError): string {
72
- const key = [
73
- data.kind,
74
- data.handled ? "handled" : "unhandled",
75
- data.message,
76
- data.filename ?? "",
77
- data.lineno ?? "",
78
- fingerprintCause(data.cause),
79
- ].join("\0");
80
-
60
+ function hashKey(key: string): string {
81
61
  let a = 2166136261;
82
62
  let b = 3598710387;
83
-
84
63
  for (let i = 0; i < key.length; i++) {
85
64
  const c = key.charCodeAt(i);
86
65
  a = Math.imul(a ^ c, 16777619);
87
66
  b = Math.imul(b ^ c, 2246822519);
88
67
  }
68
+ return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0).toString(16).padStart(8, "0")}`;
69
+ }
89
70
 
90
- return `err_${(a >>> 0).toString(16).padStart(8, "0")}${(b >>> 0)
91
- .toString(16)
92
- .padStart(8, "0")}`;
71
+ function readBuildId(): string | undefined {
72
+ const v = (globalThis as { __SOURCEMAPS_BUILD__?: { buildId?: unknown } })
73
+ .__SOURCEMAPS_BUILD__?.buildId;
74
+ return typeof v === "string" && v.trim() ? v : undefined;
93
75
  }
94
76
 
95
77
  export default class ErrorTracker {
96
78
  private readonly endpoint: string;
97
- private readonly handled = new WeakSet<Error>();
79
+ private readonly seen = new WeakSet<object>();
98
80
  private readonly queue = new Map<string, ErrorTracking>();
99
81
  private timer: ReturnType<typeof setInterval> | null = null;
100
82
  private started = false;
101
83
  private flushing = false;
102
84
 
103
85
  constructor(private readonly options: ErrorTrackingOptions) {
104
- this.endpoint = webEventsUrl(options.baseUrl);
105
- }
106
-
107
- private get debug(): boolean {
108
- return this.options.debug ?? false;
109
- }
110
-
111
- private get flushInterval(): number {
112
- return this.options.flushInterval ?? 5000;
113
- }
114
-
115
- private get maxQueueSize(): number {
116
- return this.options.maxQueueSize ?? 50;
86
+ this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.events}`;
117
87
  }
118
88
 
119
89
  private log(...args: unknown[]): void {
120
- if (this.debug) console.log("[ErrorTracker]", ...args);
90
+ if (this.options.debug) console.log("[ErrorTracker]", ...args);
121
91
  }
122
92
 
123
93
  start(): void {
124
94
  if (this.started || typeof window === "undefined") return;
125
95
  this.started = true;
126
-
127
96
  window.addEventListener("error", this.onError);
128
97
  window.addEventListener("unhandledrejection", this.onRejection);
129
- document.addEventListener("visibilitychange", this.onVisibilityChange);
130
- window.addEventListener("pagehide", this.requestFlush);
131
-
132
- this.timer = setInterval(this.requestFlush, this.flushInterval);
133
- this.log("Started listening for errors");
98
+ window.addEventListener("pagehide", this.scheduleFlush);
99
+ document.addEventListener("visibilitychange", this.onVisibility);
100
+ this.timer = setInterval(
101
+ this.scheduleFlush,
102
+ this.options.flushInterval ?? 5000,
103
+ );
104
+ this.log("Started");
134
105
  }
135
106
 
136
107
  stop(): void {
137
108
  if (!this.started || typeof window === "undefined") return;
138
109
  this.started = false;
139
-
140
110
  window.removeEventListener("error", this.onError);
141
111
  window.removeEventListener("unhandledrejection", this.onRejection);
142
- document.removeEventListener("visibilitychange", this.onVisibilityChange);
143
- window.removeEventListener("pagehide", this.requestFlush);
144
-
112
+ window.removeEventListener("pagehide", this.scheduleFlush);
113
+ document.removeEventListener("visibilitychange", this.onVisibility);
145
114
  if (this.timer) clearInterval(this.timer);
146
115
  this.timer = null;
147
-
148
- this.requestFlush();
149
- this.log("Stopped listening for errors");
116
+ this.scheduleFlush();
117
+ this.log("Stopped");
150
118
  }
151
119
 
152
120
  captureError(error: Error): void {
153
- this.record(
154
- {
155
- kind: "error",
156
- message: error.message,
157
- stack: error.stack,
158
- handled: true,
159
- cause: serializeCause(error.cause),
160
- },
161
- error,
162
- );
121
+ this.capture("error", error.message, true, error);
163
122
  }
164
123
 
165
124
  private onError = (event: ErrorEvent): void => {
166
- const err = event.error;
167
-
168
- this.record(
169
- {
170
- kind: "error",
171
- message: event.message || "Unknown error",
172
- filename: event.filename || undefined,
173
- lineno: event.lineno || undefined,
174
- stack: err instanceof Error ? err.stack : undefined,
175
- handled: false,
176
- cause: err instanceof Error ? serializeCause(err.cause) : undefined,
177
- },
178
- err,
125
+ this.capture(
126
+ "error",
127
+ event.message || "Unknown error",
128
+ false,
129
+ event.error,
130
+ event.filename || "",
131
+ event.lineno,
179
132
  );
180
133
  };
181
134
 
182
135
  private onRejection = (event: PromiseRejectionEvent): void => {
183
136
  const reason = event.reason;
184
-
185
- this.record(
186
- {
187
- kind: "unhandledrejection",
188
- message: reason.message || "Unhandled promise rejection",
189
- stack: reason.stack || undefined,
190
- handled: false,
191
- cause: serializeCause(reason.cause),
192
- },
193
- reason,
194
- );
137
+ const message =
138
+ reason instanceof Error
139
+ ? reason.message
140
+ : typeof reason === "string"
141
+ ? reason
142
+ : "Unhandled promise rejection";
143
+ this.capture("unhandledrejection", message, false, reason);
195
144
  };
196
145
 
197
- private onVisibilityChange = (): void => {
198
- if (document.visibilityState === "hidden") this.requestFlush();
146
+ private onVisibility = (): void => {
147
+ if (document.visibilityState === "hidden") this.scheduleFlush();
199
148
  };
200
149
 
201
- private requestFlush = (): void => {
150
+ private scheduleFlush = (): void => {
202
151
  void this.flush();
203
152
  };
204
153
 
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
-
214
- private record(data: NormalizedError, original?: unknown): void {
215
- if (original instanceof Error) {
216
- if (this.handled.has(original)) {
217
- this.log("Skipping duplicate:", original.message);
154
+ private capture(
155
+ kind: Kind,
156
+ message: string,
157
+ handled: boolean,
158
+ source: unknown,
159
+ filename = "",
160
+ lineno: number | "" = "",
161
+ ): void {
162
+ const stackText = source instanceof Error ? (source.stack ?? "") : "";
163
+ if (EXTENSION_RE.test(`${filename}\n${stackText}`)) return;
164
+
165
+ if (typeof source === "object" && source !== null) {
166
+ if (this.seen.has(source)) {
167
+ this.log("Skipping duplicate:", message);
218
168
  return;
219
169
  }
220
- this.handled.add(original);
170
+ this.seen.add(source);
221
171
  }
222
172
 
223
- if (EXTENSION_URL.test(`${data.filename ?? ""}\n${data.stack ?? ""}`)) {
224
- return;
173
+ const entry: ErrorEntry = {
174
+ error: kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
175
+ message,
176
+ handled,
177
+ };
178
+ if (source instanceof Error) {
179
+ entry.stack = parseStack(source.stack);
180
+ const cause = serializeCause(source.cause);
181
+ if (cause) entry.cause = cause;
225
182
  }
226
183
 
227
- const hash = hashError(data);
228
- const existing = this.queue.get(hash);
184
+ const key = hashKey(
185
+ `${kind}\0${handled ? "handled" : "unhandled"}\0${message}\0${filename}\0${lineno}\0${fingerprintCause(entry.cause)}`,
186
+ );
229
187
 
188
+ const existing = this.queue.get(key);
230
189
  if (existing) {
231
- existing.count += 1;
190
+ existing.count++;
232
191
  } else {
233
- this.queue.set(hash, {
234
- hash,
235
- count: 1,
236
- error:
237
- data.kind === "unhandledrejection" ? "UnhandledRejection" : "Error",
238
- message: data.message,
239
- stack: parseStack(data.stack),
240
- handled: data.handled,
241
- ...(data.cause ? { cause: data.cause } : {}),
242
- });
192
+ this.queue.set(key, { hash: key, count: 1, ...entry });
243
193
  }
244
194
 
245
- this.log("Captured error:", data);
195
+ this.log("Captured:", entry);
246
196
 
247
- if (this.queue.size >= this.maxQueueSize) {
248
- this.requestFlush();
197
+ if (this.queue.size >= (this.options.maxQueueSize ?? 50)) {
198
+ this.scheduleFlush();
249
199
  }
250
200
  }
251
201
 
@@ -256,22 +206,12 @@ export default class ErrorTracker {
256
206
  this.queue.clear();
257
207
  this.flushing = true;
258
208
 
259
- const identifier = getAnonymousId();
260
- const sourcemapsBuild = (
261
- globalThis as {
262
- __SOURCEMAPS_BUILD__?: { buildId?: unknown };
263
- }
264
- ).__SOURCEMAPS_BUILD__;
265
-
266
- const buildId =
267
- typeof sourcemapsBuild?.buildId === "string" &&
268
- sourcemapsBuild.buildId.trim()
269
- ? sourcemapsBuild.buildId
270
- : undefined;
209
+ const userId = getAnonymousId();
210
+ const buildId = readBuildId();
271
211
 
272
212
  const payload = JSON.stringify({
273
213
  token: this.options.siteKey,
274
- ...(identifier ? { userId: identifier } : {}),
214
+ ...(userId ? { userId } : {}),
275
215
  sessionId: getOrCreateSessionId(),
276
216
  ...(buildId ? { buildId } : {}),
277
217
  sdkName: this.options.sdkName ?? SDK_NAME,
@@ -285,21 +225,28 @@ export default class ErrorTracker {
285
225
  errors,
286
226
  });
287
227
 
288
- this.log("Flushing errors:", errors);
289
- this.log("Payload:", payload);
228
+ this.log("Flushing:", errors);
290
229
 
291
230
  try {
292
231
  const sent = await sendData({
293
232
  url: this.endpoint,
294
233
  data: payload,
295
- debug: this.debug,
234
+ debug: this.options.debug,
296
235
  debugPrefix: "[ErrorTracker]",
297
236
  });
298
- if (!sent) this.restoreErrors(errors);
237
+ if (!sent) this.requeue(errors);
299
238
  } catch {
300
- this.restoreErrors(errors);
239
+ this.requeue(errors);
301
240
  } finally {
302
241
  this.flushing = false;
303
242
  }
304
243
  }
244
+
245
+ private requeue(errors: ErrorTracking[]): void {
246
+ for (const e of errors) {
247
+ const existing = this.queue.get(e.hash);
248
+ if (existing) existing.count += e.count;
249
+ else this.queue.set(e.hash, e);
250
+ }
251
+ }
305
252
  }
@@ -1,4 +1,4 @@
1
- import { featureFlagsCheckUrl } from "./utils/api-urls";
1
+ import { normalizeFeatureFlagsBaseUrl, URLS } from "./utils/api-urls";
2
2
 
3
3
  export type FeatureFlagEvaluation = {
4
4
  value: string;
@@ -12,6 +12,7 @@ export type FeatureFlagCheckContext = {
12
12
  projectId?: string;
13
13
  identifier?: string;
14
14
  externalId?: string;
15
+ sessionId?: string;
15
16
  attributes?: Record<string, unknown>;
16
17
  signal?: AbortSignal;
17
18
  };
@@ -28,6 +29,7 @@ export async function fetchFeatureFlagEvaluation(
28
29
  const projectId = context.projectId;
29
30
  const identifier = context.identifier;
30
31
  const externalId = context.externalId;
32
+ const sessionId = context.sessionId;
31
33
 
32
34
  if (!projectToken && !projectId) {
33
35
  throw new Error("feature flag check requires projectToken or projectId");
@@ -41,6 +43,7 @@ export async function fetchFeatureFlagEvaluation(
41
43
  ...(projectId ? { projectId } : {}),
42
44
  ...(identifier ? { identifier } : {}),
43
45
  ...(externalId ? { externalId } : {}),
46
+ ...(sessionId ? { sessionId } : {}),
44
47
  };
45
48
 
46
49
  if (context.attributes && Object.keys(context.attributes).length > 0) {
@@ -54,7 +57,7 @@ export async function fetchFeatureFlagEvaluation(
54
57
  headers.Authorization = `Bearer ${projectToken}`;
55
58
  }
56
59
 
57
- const url = featureFlagsCheckUrl(context.baseUrl);
60
+ const url = `${normalizeFeatureFlagsBaseUrl(context.baseUrl)}${URLS.flags}`;
58
61
  const res = await fetch(url, {
59
62
  method: "POST",
60
63
  headers,
package/src/replay.ts CHANGED
@@ -6,7 +6,7 @@ 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 { replayEventsUrl } from "./utils/api-urls";
9
+ import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
10
10
  import {
11
11
  getAnonymousId,
12
12
  getOrCreateSessionId,
@@ -96,7 +96,7 @@ export default class ReplayTracker {
96
96
  private sending = false;
97
97
 
98
98
  constructor(private readonly options: ReplayTrackerOptions) {
99
- this.endpoint = replayEventsUrl(options.baseUrl);
99
+ this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.replay}`;
100
100
  this.sampled =
101
101
  Math.random() * 100 <
102
102
  normalizeSamplingPercentage(options.samplingPercentage);
@@ -10,28 +10,15 @@ export function normalizeAnalyticsBaseUrl(input?: string): string {
10
10
  return stripTrailingSlashes(input) || DEFAULT_BASE;
11
11
  }
12
12
 
13
- export function webEventsUrl(baseUrl?: string): string {
14
- return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/web`;
15
- }
16
-
17
- export function identifyEventsUrl(baseUrl?: string): string {
18
- return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/identify`;
19
- }
20
-
21
- export function replayEventsUrl(baseUrl?: string): string {
22
- return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/replay`;
23
- }
24
-
25
- export function webVitalsEventsUrl(baseUrl?: string): string {
26
- return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/vitals`;
27
- }
13
+ export const URLS = {
14
+ events: "/v1/web",
15
+ identify: "/v1/identify",
16
+ replay: "/v1/replay",
17
+ vitals: "/v1/vitals",
18
+ flags: "/v1/check",
19
+ } as const;
28
20
 
29
21
  export function normalizeFeatureFlagsBaseUrl(input?: string): string {
30
22
  if (input === undefined || input === "") return DEFAULT_FEATURE_FLAGS_BASE;
31
23
  return stripTrailingSlashes(input) || DEFAULT_FEATURE_FLAGS_BASE;
32
24
  }
33
-
34
- export function featureFlagsCheckUrl(baseUrl?: string): string {
35
- const base = normalizeFeatureFlagsBaseUrl(baseUrl);
36
- return `${base}/v1/check`;
37
- }
package/src/web-vitals.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Metric, MetricWithAttribution } from "web-vitals";
2
- import { webVitalsEventsUrl } from "./utils/api-urls";
2
+ import { normalizeAnalyticsBaseUrl, URLS } from "./utils/api-urls";
3
3
  import { getOrCreateSessionId } from "./utils/identifiers";
4
4
  import { normalizeSamplingPercentage } from "./utils/types";
5
5
 
@@ -12,64 +12,23 @@ export interface WebVitalsOptions {
12
12
  }
13
13
 
14
14
  type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
15
+ type AnyMetric = Metric | MetricWithAttribution;
15
16
 
16
- type MetricState = {
17
+ type MetricPayload = {
18
+ metric: MetricName;
17
19
  value: number;
18
20
  attributes: Record<string, unknown>;
19
21
  };
20
22
 
21
- type AnyMetric = Metric | MetricWithAttribution;
22
- type ObserverOptions = {
23
- reportAllChanges?: boolean;
24
- };
25
- type Observer = (
26
- cb: (metric: AnyMetric) => void,
27
- opts?: ObserverOptions,
28
- ) => void;
29
- type ObserverTuple = [Observer, Observer, Observer, Observer, Observer];
30
- type ObserverConfig = {
31
- observe: Observer;
32
- options?: ObserverOptions;
33
- };
34
-
35
- async function loadObservers(attribution: boolean): Promise<ObserverConfig[]> {
36
- const { onCLS, onFCP, onINP, onLCP, onTTFB } = attribution
37
- ? await import("web-vitals/attribution")
38
- : await import("web-vitals");
39
- return createObserverConfigs([onCLS, onFCP, onINP, onLCP, onTTFB]);
40
- }
41
-
42
- function createObserverConfigs([
43
- onCLS,
44
- onFCP,
45
- onINP,
46
- onLCP,
47
- onTTFB,
48
- ]: ObserverTuple): ObserverConfig[] {
49
- return [
50
- { observe: onCLS, options: { reportAllChanges: true } },
51
- { observe: onFCP },
52
- { observe: onINP, options: { reportAllChanges: true } },
53
- { observe: onLCP, options: { reportAllChanges: true } },
54
- { observe: onTTFB },
55
- ];
56
- }
57
-
58
23
  export default class WebVitalsTracker {
59
24
  private readonly endpoint: string;
60
- private readonly metricsByUrl = new Map<
61
- string,
62
- Map<MetricName, MetricState>
63
- >();
64
- private readonly pendingFlushUrls = new Set<string>();
65
25
  private readonly sampled: boolean;
26
+ private readonly metrics = new Map<MetricName, MetricPayload>();
66
27
  private started = false;
67
- private flushing = false;
68
- private finalFlushRequested = false;
69
- private currentUrl = "";
28
+ private url = "";
70
29
 
71
30
  constructor(private readonly options: WebVitalsOptions) {
72
- this.endpoint = webVitalsEventsUrl(options.baseUrl);
31
+ this.endpoint = `${normalizeAnalyticsBaseUrl(options.baseUrl)}${URLS.vitals}`;
73
32
  this.sampled =
74
33
  Math.random() * 100 <
75
34
  normalizeSamplingPercentage(options.samplingPercentage);
@@ -83,77 +42,68 @@ export default class WebVitalsTracker {
83
42
  if (this.debug) console.log("[WebVitals]", ...args);
84
43
  }
85
44
 
86
- start(): void {
45
+ async start(): Promise<void> {
87
46
  if (this.started || typeof window === "undefined") return;
88
47
 
89
48
  this.started = true;
90
- this.currentUrl = window.location.href;
49
+ this.url = window.location.href;
91
50
 
92
- document.addEventListener("visibilitychange", this.onVisibilityChange, {
51
+ document.addEventListener("visibilitychange", this.onHidden, {
93
52
  passive: true,
94
53
  });
95
54
  window.addEventListener("pagehide", this.onPageHide, { passive: true });
96
55
 
56
+ const mod = this.options.attribution
57
+ ? await import("web-vitals/attribution")
58
+ : await import("web-vitals");
59
+
60
+ if (!this.started) return;
61
+
62
+ mod.onCLS(this.capture, { reportAllChanges: true });
63
+ mod.onINP(this.capture, { reportAllChanges: true });
64
+ mod.onLCP(this.capture, { reportAllChanges: true });
65
+ mod.onFCP(this.capture);
66
+ mod.onTTFB(this.capture);
67
+
97
68
  this.log("Tracking started");
98
- void this.observe();
99
69
  }
100
70
 
101
71
  stop(): void {
102
72
  if (!this.started || typeof window === "undefined") return;
103
73
 
104
74
  this.started = false;
105
- this.cleanupListeners();
106
- this.queueFlush(this.currentUrl, true);
75
+ this.removeListeners();
76
+ void this.flush();
107
77
  }
108
78
 
109
79
  trackPageChange(url?: string): void {
110
80
  if (!this.started || typeof window === "undefined") return;
111
- const nextUrl = url ?? window.location.href;
112
- if (nextUrl === this.currentUrl) return;
113
81
 
114
- const previousUrl = this.currentUrl;
115
- this.currentUrl = nextUrl;
116
- this.queueFlush(previousUrl);
117
- }
82
+ const nextUrl = url ?? window.location.href;
83
+ if (nextUrl === this.url) return;
118
84
 
119
- private cleanupListeners(): void {
120
- document.removeEventListener("visibilitychange", this.onVisibilityChange);
121
- window.removeEventListener("pagehide", this.onPageHide);
85
+ void this.flush();
86
+ this.url = nextUrl;
122
87
  }
123
88
 
124
- private async observe(): Promise<void> {
125
- try {
126
- const observers = await loadObservers(this.options.attribution ?? false);
127
-
128
- if (!this.started) return;
129
-
130
- for (const { observe, options } of observers) {
131
- observe(this.captureMetric, options);
132
- }
133
- } catch (error) {
134
- this.log("Failed to load web-vitals", error);
135
- }
136
- }
137
-
138
- private onVisibilityChange = (): void => {
89
+ private onHidden = (): void => {
139
90
  if (document.visibilityState === "hidden") {
140
- this.queueFlush(this.currentUrl, true);
91
+ void this.flush();
141
92
  }
142
93
  };
143
94
 
144
95
  private onPageHide = (): void => {
145
- this.queueFlush(this.currentUrl, true);
96
+ void this.flush();
146
97
  };
147
98
 
148
- private captureMetric = (metric: AnyMetric): void => {
99
+ private capture = (metric: AnyMetric): void => {
149
100
  if (!this.started || !this.sampled) return;
150
101
 
151
102
  const name = metric.name as MetricName;
152
- const attribution =
153
- (metric as MetricWithAttribution).attribution ?? undefined;
154
- const metrics = this.metricsForUrl(this.currentUrl);
103
+ const attribution = (metric as MetricWithAttribution).attribution;
155
104
 
156
- metrics.set(name, {
105
+ this.metrics.set(name, {
106
+ metric: name,
157
107
  value: metric.value,
158
108
  attributes: {
159
109
  id: metric.id,
@@ -163,99 +113,47 @@ export default class WebVitalsTracker {
163
113
  ...(attribution ?? {}),
164
114
  },
165
115
  });
166
-
167
- this.log(`${name} captured: ${metric.value}`);
168
116
  };
169
117
 
170
- private metricsForUrl(url: string): Map<MetricName, MetricState> {
171
- let metrics = this.metricsByUrl.get(url);
172
- if (!metrics) {
173
- metrics = new Map();
174
- this.metricsByUrl.set(url, metrics);
175
- }
176
- return metrics;
177
- }
178
-
179
- private queueFlush(url: string, final = false): void {
180
- this.finalFlushRequested ||= final;
181
- this.pendingFlushUrls.add(url);
182
- void this.flushPending();
183
- }
184
-
185
- private async flushPending(): Promise<void> {
186
- if (this.flushing) return;
187
- if (typeof window === "undefined" || typeof fetch !== "function") {
188
- this.finishFinalFlushIfNeeded();
189
- return;
190
- }
191
-
192
- this.flushing = true;
193
-
194
- try {
195
- const urls = [...this.pendingFlushUrls];
196
- this.pendingFlushUrls.clear();
197
- for (const url of urls) {
198
- if (!(await this.sendMetricsForUrl(url)))
199
- this.pendingFlushUrls.add(url);
200
- }
201
- } finally {
202
- this.flushing = false;
203
- this.finishFinalFlushIfNeeded();
204
- }
205
- }
206
-
207
- private async sendMetricsForUrl(url: string): Promise<boolean> {
208
- const metrics = this.metricsByUrl.get(url);
209
- if (!metrics || metrics.size === 0) return true;
210
-
211
- const vitals = [...metrics.entries()].map(([metric, data]) => ({
212
- metric,
213
- value: data.value,
214
- attributes: data.attributes,
215
- }));
216
- const body = JSON.stringify({
217
- sessionId: getOrCreateSessionId(),
218
- vitals,
219
- metadata: { url },
220
- });
118
+ private async flush(): Promise<void> {
119
+ if (typeof window === "undefined" || typeof fetch !== "function") return;
120
+ if (!this.metrics.size) return;
221
121
 
222
- this.log(`Sending metrics for ${url} (${vitals.length})`);
122
+ const vitals = Array.from(this.metrics.values());
123
+ const url = this.url;
124
+ this.metrics.clear();
223
125
 
224
126
  try {
225
127
  const controller = new AbortController();
226
- const timeout = window.setTimeout(() => controller.abort(), 3000);
227
-
228
- try {
229
- const response = await fetch(this.endpoint, {
230
- method: "POST",
231
- body,
232
- headers: {
233
- "Content-Type": "application/json",
234
- Authorization: `Bearer ${this.options.siteKey}`,
235
- },
236
- keepalive: true,
237
- signal: controller.signal,
238
- });
239
-
240
- if (!response.ok) {
241
- throw new Error(`HTTP ${response.status}`);
242
- }
243
-
244
- this.metricsByUrl.delete(url);
245
- } finally {
246
- clearTimeout(timeout);
128
+ const timeoutId = window.setTimeout(() => controller.abort(), 3000);
129
+
130
+ const response = await fetch(this.endpoint, {
131
+ method: "POST",
132
+ body: JSON.stringify({
133
+ sessionId: getOrCreateSessionId(),
134
+ vitals,
135
+ metadata: { url },
136
+ }),
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Authorization: `Bearer ${this.options.siteKey}`,
140
+ },
141
+ keepalive: true,
142
+ signal: controller.signal,
143
+ });
144
+
145
+ clearTimeout(timeoutId);
146
+
147
+ if (!response.ok) {
148
+ throw new Error(`HTTP ${response.status}`);
247
149
  }
248
- return true;
249
150
  } catch (error) {
250
151
  this.log("Failed to send metrics", error);
251
- return false;
252
152
  }
253
153
  }
254
154
 
255
- private finishFinalFlushIfNeeded(): void {
256
- if (!this.finalFlushRequested) return;
257
- this.finalFlushRequested = false;
258
- this.started = false;
259
- this.cleanupListeners();
155
+ private removeListeners(): void {
156
+ document.removeEventListener("visibilitychange", this.onHidden);
157
+ window.removeEventListener("pagehide", this.onPageHide);
260
158
  }
261
159
  }
@@ -362,6 +362,7 @@ describe("WebAnalytics lifecycle", () => {
362
362
  expect(checkCall?.body).toContain('"key":"beta"');
363
363
  expect(checkCall?.body).toContain('"externalId":"user_1"');
364
364
  expect(checkCall?.body).toContain('"identifier":');
365
+ expect(checkCall?.body).toContain('"sessionId":');
365
366
  });
366
367
 
367
368
  test("navigation sends page leave and navigation pageview", async () => {
@@ -1 +0,0 @@
1
- const e=`https://metrics.faststats.dev`,t=`https://flags.faststats.dev`;function n(e){return e.replace(/\/+$/,``)}function r(t){return t===void 0||t===``?e:n(t)||e}function i(e){return`${r(e)}/v1/web`}function a(e){return`${r(e)}/v1/identify`}function o(e){return`${r(e)}/v1/replay`}function s(e){return`${r(e)}/v1/vitals`}function c(e){return e===void 0||e===``?t:n(e)||t}function l(e){return`${c(e)}/v1/check`}export{o as a,c as i,a as n,i as o,r,s,l as t};
@@ -1,2 +0,0 @@
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};
@@ -1 +0,0 @@
1
- import{t as e}from"./rolldown-runtime-MP-BAFHD.js";import{t}from"./api-urls-BrkcoElX.js";var n=e({fetchFeatureFlagEvaluation:()=>r});async function r(e,n){if(n.projectToken&&n.projectId)throw Error(`provide either projectToken or projectId, not both`);let r=n.projectToken,i=n.projectId,a=n.identifier,o=n.externalId;if(!r&&!i)throw Error(`feature flag check requires projectToken or projectId`);if(!a&&!o)throw Error(`feature flag check requires serverId or externalId`);let s={key:e,...i?{projectId:i}:{},...a?{identifier:a}:{},...o?{externalId:o}:{}};n.attributes&&Object.keys(n.attributes).length>0&&(s.attributes=n.attributes);let c={"Content-Type":`application/json`};r&&(c.Authorization=`Bearer ${r}`);let l=t(n.baseUrl),u=await fetch(l,{method:`POST`,headers:c,body:JSON.stringify(s),credentials:`omit`,signal:n.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()}export{r as n,n as t};
@@ -1 +0,0 @@
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};
@@ -1 +0,0 @@
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};