@faststats/web 0.1.9 → 0.2.0

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.
@@ -1,4 +1,5 @@
1
1
  const DEFAULT_BASE = "https://metrics.faststats.dev";
2
+ const DEFAULT_FEATURE_FLAGS_BASE = "https://flags.faststats.dev";
2
3
 
3
4
  function stripTrailingSlashes(s: string): string {
4
5
  return s.replace(/\/+$/, "");
@@ -24,3 +25,17 @@ export function replayEventsUrl(baseUrl?: string): string {
24
25
  export function webVitalsEventsUrl(baseUrl?: string): string {
25
26
  return `${normalizeAnalyticsBaseUrl(baseUrl)}/v1/vitals`;
26
27
  }
28
+
29
+ export function normalizeFeatureFlagsBaseUrl(input?: string): string {
30
+ if (input === undefined || input === "") return DEFAULT_FEATURE_FLAGS_BASE;
31
+ return stripTrailingSlashes(input) || DEFAULT_FEATURE_FLAGS_BASE;
32
+ }
33
+
34
+ export function featureFlagsCheckUrl(
35
+ baseUrl: string | undefined,
36
+ flagKey: string,
37
+ ): string {
38
+ const base = normalizeFeatureFlagsBaseUrl(baseUrl);
39
+ const encoded = encodeURIComponent(flagKey);
40
+ return `${base}/v1/check/${encoded}`;
41
+ }
package/src/web-vitals.ts CHANGED
@@ -22,133 +22,91 @@ type MetricName = "CLS" | "INP" | "LCP" | "FCP" | "TTFB";
22
22
  type MetricState = {
23
23
  value: number;
24
24
  attributes: Record<string, unknown>;
25
- final: boolean;
26
25
  };
27
26
 
28
- class WebVitalsTracker {
29
- private readonly endpoint: string;
30
- private readonly siteKey: string;
31
- private readonly debug: boolean;
32
- private readonly samplingPercentage: number;
27
+ const observeVitals = [onCLS, onINP, onLCP, onFCP, onTTFB];
33
28
 
29
+ export default class WebVitalsTracker {
30
+ private readonly endpoint: string;
31
+ private readonly metrics = new Map<MetricName, MetricState>();
32
+ private readonly sampled: boolean;
34
33
  private started = false;
35
- private readonly sessionSamplingSeed: number;
36
- private readonly metricsMap = new Map<MetricName, MetricState>();
37
34
  private flushed = false;
38
- private readonly handleVisibilityChange = (): void => {
39
- if (document.visibilityState === "hidden") {
40
- this.finalizeAndFlush();
41
- }
42
- };
43
- private readonly handlePageHide = (): void => {
44
- this.finalizeAndFlush();
45
- };
46
35
 
47
- constructor(options: WebVitalsOptions) {
48
- this.siteKey = options.siteKey;
36
+ constructor(private readonly options: WebVitalsOptions) {
49
37
  this.endpoint = webVitalsEventsUrl(options.baseUrl);
50
- this.debug = options.debug ?? false;
51
- this.samplingPercentage = normalizeSamplingPercentage(
52
- options.samplingPercentage,
53
- );
54
- this.sessionSamplingSeed = Math.random() * 100;
38
+ this.sampled =
39
+ Math.random() * 100 <
40
+ normalizeSamplingPercentage(options.samplingPercentage);
41
+ }
42
+
43
+ private get debug(): boolean {
44
+ return this.options.debug ?? false;
45
+ }
46
+
47
+ private log(...args: unknown[]): void {
48
+ if (this.debug) console.log("[WebVitals]", ...args);
55
49
  }
56
50
 
57
51
  start(): void {
58
52
  if (this.started || typeof window === "undefined") return;
59
-
60
53
  this.started = true;
61
54
  this.flushed = false;
62
55
 
63
- document.addEventListener("visibilitychange", this.handleVisibilityChange);
64
- window.addEventListener("pagehide", this.handlePageHide);
56
+ document.addEventListener("visibilitychange", this.onVisibilityChange);
57
+ window.addEventListener("pagehide", this.flush);
65
58
 
66
- if (this.debug) {
67
- console.log("[WebVitals] Tracking started");
68
- }
59
+ this.log("Tracking started");
69
60
 
70
- onCLS((metric) => this.captureMetric(metric));
71
- onINP((metric) => this.captureMetric(metric));
72
- onLCP((metric) => this.captureMetric(metric));
73
- onFCP((metric) => this.captureMetric(metric));
74
- onTTFB((metric) => this.captureMetric(metric));
61
+ for (const observe of observeVitals) {
62
+ observe(this.captureMetric);
63
+ }
75
64
  }
76
65
 
77
66
  stop(): void {
78
67
  if (!this.started || typeof window === "undefined") return;
79
68
  this.started = false;
80
- document.removeEventListener(
81
- "visibilitychange",
82
- this.handleVisibilityChange,
83
- );
84
- window.removeEventListener("pagehide", this.handlePageHide);
85
- this.finalizeAndFlush();
86
- }
87
69
 
88
- private captureMetric(metric: MetricWithAttribution): void {
89
- if (this.flushed) return;
70
+ document.removeEventListener("visibilitychange", this.onVisibilityChange);
71
+ window.removeEventListener("pagehide", this.flush);
90
72
 
91
- if (
92
- this.samplingPercentage < 100 &&
93
- this.sessionSamplingSeed >= this.samplingPercentage
94
- ) {
95
- return;
96
- }
73
+ this.flush();
74
+ }
97
75
 
98
- const metricName = metric.name as MetricName;
76
+ private onVisibilityChange = (): void => {
77
+ if (document.visibilityState === "hidden") this.flush();
78
+ };
99
79
 
100
- const attributes: Record<string, unknown> = {
101
- id: metric.id,
102
- rating: metric.rating,
103
- delta: metric.delta,
104
- navigationType: metric.navigationType,
105
- ...(metric.attribution ?? {}),
106
- };
80
+ private captureMetric = (metric: MetricWithAttribution): void => {
81
+ if (this.flushed || !this.sampled) return;
107
82
 
108
- const isFinal = metricName === "FCP" || metricName === "TTFB";
83
+ const name = metric.name as MetricName;
109
84
 
110
- this.metricsMap.set(metricName, {
85
+ this.metrics.set(name, {
111
86
  value: metric.value,
112
- attributes,
113
- final: isFinal,
87
+ attributes: {
88
+ id: metric.id,
89
+ rating: metric.rating,
90
+ delta: metric.delta,
91
+ navigationType: metric.navigationType,
92
+ ...(metric.attribution ?? {}),
93
+ },
114
94
  });
115
95
 
116
- if (this.debug) {
117
- console.log(
118
- `[WebVitals] ${metricName} captured: ${metric.value}` +
119
- (isFinal ? " (final)" : ""),
120
- );
121
- }
122
- }
123
-
124
- private finalizeAndFlush(): void {
125
- if (this.flushed || this.metricsMap.size === 0) {
126
- return;
127
- }
128
-
129
- for (const [name, metric] of this.metricsMap.entries()) {
130
- if (!metric.final) {
131
- metric.final = true;
132
-
133
- if (this.debug) {
134
- console.log(`[WebVitals] ${name} finalized: ${metric.value}`);
135
- }
136
- }
137
- }
138
-
139
- this.flushWithBeacon();
140
- }
96
+ this.log(`${name} captured: ${metric.value}`);
97
+ };
141
98
 
142
- private buildPayload(): { body: string; count: number } | null {
143
- if (this.metricsMap.size === 0) return null;
99
+ private flush = (): void => {
100
+ if (this.flushed || this.metrics.size === 0) return;
101
+ this.flushed = true;
144
102
 
145
- const vitals = Array.from(this.metricsMap.entries()).map(
146
- ([metric, data]) => ({
147
- metric,
148
- value: data.value,
149
- attributes: data.attributes,
150
- }),
151
- );
103
+ const vitals = [...this.metrics.entries()].map(([metric, data]) => ({
104
+ metric,
105
+ value: data.value,
106
+ attributes: data.attributes,
107
+ }));
108
+ const metricNames = vitals.map((v) => v.metric).join(", ");
109
+ this.metrics.clear();
152
110
 
153
111
  const body = JSON.stringify({
154
112
  sessionId: getOrCreateSessionId(),
@@ -158,40 +116,20 @@ class WebVitalsTracker {
158
116
  },
159
117
  });
160
118
 
161
- return {
162
- body,
163
- count: vitals.length,
164
- };
165
- }
166
-
167
- private flushWithBeacon(): void {
168
- const payload = this.buildPayload();
169
- if (!payload) return;
119
+ this.log(`Sending final metrics (${vitals.length}): ${metricNames}`);
170
120
 
171
- this.flushed = true;
172
-
173
- if (this.debug) {
174
- const names = Array.from(this.metricsMap.keys()).join(", ");
175
- console.log(
176
- `[WebVitals] Sending final metrics (${payload.count}): ${names}`,
177
- );
178
- }
179
121
  if (typeof fetch !== "function") return;
180
122
 
181
123
  fetch(this.endpoint, {
182
124
  method: "POST",
183
- body: payload.body,
125
+ body,
184
126
  headers: {
185
127
  "Content-Type": "application/json",
186
- Authorization: `Bearer ${this.siteKey}`,
128
+ Authorization: `Bearer ${this.options.siteKey}`,
187
129
  },
188
130
  keepalive: true,
189
131
  }).catch(() => {
190
- if (this.debug) {
191
- console.warn("[WebVitals] Failed to send metrics");
192
- }
132
+ this.log("Failed to send metrics");
193
133
  });
194
- }
134
+ };
195
135
  }
196
-
197
- export default WebVitalsTracker;
@@ -1 +0,0 @@
1
- import{a as e,c as t,f as n,i as r,l as i,n as a,o,r as s,s as c,t as l,u}from"./types-8FXsUqbi.js";let d=null,f;function p(){return d}function m(e,t){typeof window>`u`||x()||d?.track(e,t??{})}function h(e,t,n){typeof window>`u`||x()||d?.identify(e,t,n??{})}function g(e=!0){typeof window>`u`||x()||d?.logout(e)}function _(e){if(d){d.setConsentMode(e);return}f=e}function v(){_(`granted`)}function y(){_(`denied`)}function b(e){typeof window>`u`||x()||d?.reportError(e)}function x(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function S(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0,keepalive:c=!0}=e;if(s&&typeof navigator<`u`&&typeof navigator.sendBeacon==`function`)try{let e=n instanceof Blob?n:typeof Blob<`u`?new Blob([n],{type:r}):n;if(navigator.sendBeacon(t,e))return a&&console.log(`${o} Sent via beacon`),!0}catch{}if(typeof fetch!=`function`)return a&&console.warn(`${o} Failed to send`),!1;try{let e=await fetch(t,{method:`POST`,body:n,headers:{"Content-Type":r,...i},keepalive:c}),s=e.ok;return a&&(s?console.log(`${o} Sent via fetch`):console.warn(`${o} Failed: ${e.status}`)),s}catch{return a&&console.warn(`${o} Failed to send`),!1}}function C(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function w(){let e={};if(!location.search)return e;let t=new URLSearchParams(location.search);for(let n of[`utm_source`,`utm_medium`,`utm_campaign`,`utm_term`,`utm_content`]){let r=t.get(n);r&&(e[n]=r)}return e}var T=class{webEndpoint;baseUrl;debug;started=!1;destroyed=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;cleanupCallbacks=[];childTrackers=[];pendingReportedErrors=[];errorTracker=null;handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.leavePage(),this.stopHeartbeat()):this.startHeartbeat()};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(e){this.options=e,this.baseUrl=u(e.baseUrl),this.webEndpoint=n(this.baseUrl),this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,f!==void 0&&(this.consentMode=f,f=void 0),(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(x()){this.log(`disabled`);return}d=this,t(this.isCookielessMode()),setTimeout(()=>void this.start(),0)}}registerCleanup(e){this.cleanupCallbacks.push(e)}addWindowListener(e,t){window.addEventListener(e,t),this.registerCleanup(()=>window.removeEventListener(e,t))}addDocumentListener(e,t){document.addEventListener(e,t),this.registerCleanup(()=>document.removeEventListener(e,t))}patchHistory(){let e=history.pushState.bind(history),t=history.replaceState.bind(history);history.pushState=(t,n,r)=>{e(t,n,r),this.navigate()},history.replaceState=(e,n,r)=>{t(e,n,r),this.navigate()},this.registerCleanup(()=>{history.pushState=e,history.replaceState=t})}ensureStarted(){return typeof window>`u`||this.destroyed||x()?!1:(this.started||this.start(),!0)}stopHeartbeat(){this.heartbeatTimer&&=(clearInterval(this.heartbeatTimer),null)}stopNavigationTimer(){this.navTimer&&=(clearTimeout(this.navTimer),null)}stopChildTrackers(){for(let e of this.childTrackers.splice(0))e.stop?.();this.errorTracker=null}getErrorTracker(){return this.errorTracker}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(d&&d!==this){this.log(`already started by another instance`);return}if(x()){this.log(`disabled`);return}this.started=!0,d=this,t(this.isCookielessMode());let e=this.options;if(e.trackErrors)try{let{default:t}=await import(`./error-Cs5wnTbZ.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug});for(n.start(),this.errorTracker=n,this.childTrackers.push(n);this.pendingReportedErrors.length>0;){let e=this.pendingReportedErrors.shift();e&&n.captureError(e)}this.log(`error loaded`)}catch(e){this.log(`failed to initialize error tracker: ${String(e)}`)}if(e.trackWebVitals)try{let{default:t}=await import(`./web-vitals-C7H69Xv3.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:l(e.webVitals?.sampling?.percentage)});n.start(),this.childTrackers.push(n),this.log(`web-vitals loaded`)}catch(e){this.log(`failed to initialize web-vitals tracker: ${String(e)}`)}if(e.trackReplay)try{let{default:t}=await import(`./replay-DWCkIH4r.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,...e.replayOptions});n.start(),this.childTrackers.push(n),this.log(`replay loaded`)}catch(e){this.log(`failed to initialize replay tracker: ${String(e)}`)}this.enterPage(),this.pageview({trigger:`load`}),this.links(),this.trackScroll(),this.startHeartbeat(),this.addDocumentListener(`visibilitychange`,this.handleVisibilityChange),this.addWindowListener(`pagehide`,this.handlePageHide),this.addWindowListener(`popstate`,this.handlePopState),e.trackHash&&this.addWindowListener(`hashchange`,this.handleHashChange),this.patchHistory()}destroy(){if(!this.destroyed){for(this.started&&typeof window<`u`&&this.leavePage(),this.pendingReportedErrors.length=0,this.stopNavigationTimer(),this.stopHeartbeat(),this.scrollHandler&&typeof window<`u`&&(window.removeEventListener(`scroll`,this.scrollHandler),this.scrollHandler=null);this.cleanupCallbacks.length>0;)this.cleanupCallbacks.pop()?.();this.stopChildTrackers(),d===this&&(d=null),this.started=!1,this.destroyed=!0}}pageview(e={}){if(!this.ensureStarted())return;let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.ensureStarted()&&this.send(e,t)}identify(e,t,n={}){if(!this.ensureStarted()||this.isCookielessMode())return;let r=e.trim(),o=t.trim();!r||!o||S({url:i(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:a(!1),externalId:r,email:o,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){this.ensureStarted()&&(e&&o(this.isCookielessMode()),c())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return a(this.isCookielessMode())}getSessionId(){return s()}reportError(e){if(this.destroyed||typeof window>`u`||x()||!(this.options.trackErrors??!1)||!this.ensureStarted())return;let t=this.getErrorTracker();if(t){t.captureError(e);return}this.pendingReportedErrors.length>=50&&this.pendingReportedErrors.shift(),this.pendingReportedErrors.push(e)}isCookielessMode(){return this.options.cookieless||this.consentMode===`denied`?!0:this.consentMode===`pending`?this.cookielessWhilePending:!1}send(e,t={}){if(typeof window>`u`||this.destroyed||x())return;let n=a(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:s(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...w(),...t}});this.log(e),S({url:this.webEndpoint,data:r,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.destroyed||this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-r()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){this.stopHeartbeat();return}e()},300*1e3)}navigate(){!this.started||this.destroyed||(this.stopNavigationTimer(),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300))}links(){let e=e=>{let t=C(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{g as a,b as c,m as d,x as i,S as l,p as n,v as o,h as r,y as s,T as t,_ as u};
@@ -1,2 +0,0 @@
1
- import{f as e,n as t,r as n}from"./types-8FXsUqbi.js";import{l as r}from"./analytics-B1kgY_Yf.js";const i=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//;function a(e){if(e)return e.split(`
2
- `).map(e=>e.trim()).filter(e=>e.length>0)}function o(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:a(e.stack),cause:o(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function s(e){return e?`${e.error}\0${e.message??``}\0${s(e.cause)}`:``}function c(e){let t=[e.type,e.handled?`handled`:`unhandled`,e.message,e.filename??``,e.lineno??``,s(e.cause)].join(`\0`),n=2166136261,r=3598710387;for(let e=0;e<t.length;e++){let i=t.charCodeAt(e);n=Math.imul(n^i,16777619),r=Math.imul(r^i,2246822519)}return`err_${(n>>>0).toString(16).padStart(8,`0`)}${(r>>>0).toString(16).padStart(8,`0`)}`}var l=class{endpoint;siteKey;debug;flushInterval;maxQueueSize;handledErrors=new WeakSet;errorCounts=new Map;handleVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush()};handlePageHide=()=>{this.flush()};flushTimer=null;started=!1;constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.flushInterval=t.flushInterval??5e3,this.maxQueueSize=t.maxQueueSize??50}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.handleErrorEvent),window.addEventListener(`unhandledrejection`,this.handleRejection),this.flushTimer=setInterval(()=>this.flush(),this.flushInterval),document.addEventListener(`visibilitychange`,this.handleVisibilityChange),window.addEventListener(`pagehide`,this.handlePageHide),this.debug&&console.log(`[ErrorTracker] Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.handleErrorEvent),window.removeEventListener(`unhandledrejection`,this.handleRejection),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),window.removeEventListener(`pagehide`,this.handlePageHide),this.flushTimer&&=(clearInterval(this.flushTimer),null),this.flush(),this.debug&&console.log(`[ErrorTracker] Stopped listening for errors`))}skipDuplicate(e){return e instanceof Error?this.handledErrors.has(e)?!0:(this.handledErrors.add(e),!1):!1}handleErrorEvent=e=>{let t=e.error;if(this.skipDuplicate(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate error:`,t.message);return}this.queueError({message:e.message||(t instanceof Error?t.message:`Unknown error`),filename:e.filename||void 0,lineno:e.lineno||void 0,colno:e.colno||void 0,stack:t instanceof Error?t.stack:void 0,type:`error`,handled:!1,cause:t instanceof Error?o(t.cause):void 0})};handleRejection=e=>{let t=e.reason;if(this.skipDuplicate(t)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate rejection:`,t instanceof Error?t.message:t);return}let n=t instanceof Error?t.message:typeof t==`string`?t:`Unhandled promise rejection`;this.queueError({message:n,stack:t instanceof Error?t.stack:void 0,type:`unhandledrejection`,handled:!1,cause:t instanceof Error?o(t.cause):void 0})};queueError(e){if(i.test(`${e.filename??``}\n${e.stack??``}`))return;this.debug&&console.log(`[ErrorTracker] Captured error:`,e);let t=c(e),n=this.errorCounts.get(t);if(n)n.count++,this.debug&&console.log(`[ErrorTracker] Incremented count for ${t} to ${n.count}`);else{let n={error:e.type===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:a(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}};this.errorCounts.set(t,{entry:n,count:1}),this.debug&&console.log(`[ErrorTracker] Queued new error: ${t}`)}this.errorCounts.size>=this.maxQueueSize&&this.flush()}captureError(e){if(this.skipDuplicate(e)){this.debug&&console.log(`[ErrorTracker] Skipping duplicate:`,e.message);return}this.queueError({message:e.message,stack:e.stack,type:`error`,handled:!0,cause:o(e.cause)})}flush(){if(this.errorCounts.size===0)return;let e=[];for(let[t,{entry:n,count:r}]of this.errorCounts)e.push({...n,hash:t,count:r});this.errorCounts.clear(),this.debug&&console.log(`[ErrorTracker] Flushing errors:`,e);let i=t(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim().length>0?a.buildId:void 0,s=JSON.stringify({token:this.siteKey,...i?{userId:i}:{},sessionId:n(),...o?{buildId:o}:{},data:{url:location.href,page:location.pathname,referrer:document.referrer||null,title:document.title},errors:e});this.debug&&console.log(`[ErrorTracker] Payload:`,s),r({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{l as default};
@@ -1 +0,0 @@
1
- import{d as e,n as t,r as n,t as r}from"./types-8FXsUqbi.js";import{l as i}from"./analytics-B1kgY_Yf.js";import{getRecordConsolePlugin as a}from"@rrweb/rrweb-plugin-console-record";import{getRecordSequentialIdPlugin as o}from"@rrweb/rrweb-plugin-sequential-id-record";import{EventType as s,record as c}from"rrweb";var l=class{endpoint;siteKey;debug;flushInterval;maxEvents;sampling;slimDOMOptions;maskAllInputs;maskInputOptions;blockClass;blockSelector;maskTextClass;maskTextSelector;checkoutEveryNms;checkoutEveryNth;samplingPercentage;recordConsole;events=[];flushTimer=null;stopRecording=void 0;started=!1;startTime=0;minLengthFlushScheduled=!1;sequenceNumber=0;pendingBatches=[];isFlushing=!1;compressionSupported=!1;sessionSamplingSeed;minReplayLengthMs;maxPendingBatches;flushScheduled=!1;flushTimeout=null;idleFlushId=null;minLengthFlushTimer=null;pendingRetryTimer=null;isProcessingPending=!1;constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.samplingPercentage=r(t.samplingPercentage),this.flushInterval=t.flushInterval??1e4,this.maxEvents=t.maxEvents??500,this.sampling=t.sampling??{mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},this.slimDOMOptions=t.slimDOMOptions??{script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0},this.maskAllInputs=t.maskAllInputs??!0,this.maskInputOptions=t.maskInputOptions??{password:!0,email:!0,tel:!0},this.blockClass=t.blockClass,this.blockSelector=t.blockSelector,this.maskTextClass=t.maskTextClass,this.maskTextSelector=t.maskTextSelector,this.checkoutEveryNms=t.checkoutEveryNms??6e4,this.checkoutEveryNth=t.checkoutEveryNth,this.recordConsole=t.recordConsole??!0,this.minReplayLengthMs=t.minReplayLengthMs??3e3,this.maxPendingBatches=t.maxPendingBatches??30,this.sessionSamplingSeed=Math.random()*100,typeof window<`u`&&(this.compressionSupported=`CompressionStream`in window)}start(){if(this.started||typeof window>`u`||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;this.started=!0,this.startTime=Date.now(),this.debug&&console.log(`[Replay] Recording started`);let e={emit:(e,t)=>this.handleEvent(e,t),sampling:this.sampling,slimDOMOptions:this.slimDOMOptions,maskAllInputs:this.maskAllInputs,checkoutEveryNms:this.checkoutEveryNms};this.maskInputOptions&&(e.maskInputOptions=this.maskInputOptions),this.blockClass&&(e.blockClass=this.blockClass),this.blockSelector&&(e.blockSelector=this.blockSelector),this.maskTextClass&&(e.maskTextClass=this.maskTextClass),this.maskTextSelector&&(e.maskTextSelector=this.maskTextSelector),this.checkoutEveryNth&&(e.checkoutEveryNth=this.checkoutEveryNth),e.plugins=[o({key:`_faststatsSeqId`}),...this.recordConsole?[a()]:[]],this.stopRecording=c(e),this.flushTimer=setInterval(()=>{this.scheduleFlush()},this.flushInterval),window.addEventListener(`beforeunload`,this.handleUnload),window.addEventListener(`pagehide`,this.handleUnload),document.addEventListener(`visibilitychange`,this.handleVisibilityChange),this.scheduleMinLengthFlush()}stop(){if(this.started){if(this.started=!1,this.debug&&console.log(`[Replay] Recording stopped`),this.stopRecording?.(),this.stopRecording=void 0,this.clearScheduledFlush(),this.clearMinLengthFlushTimer(),this.clearPendingRetryTimer(),this.flushTimer&&=(clearInterval(this.flushTimer),null),window.removeEventListener(`beforeunload`,this.handleUnload),window.removeEventListener(`pagehide`,this.handleUnload),document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),!this.hasReachedMinLength()){this.events=[],this.debug&&console.log(`[Replay] Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush()}}handleEvent(e,t){this.events.push(e),t||this.events.length>=this.maxEvents?this.scheduleFlush():(e.type===s.FullSnapshot&&this.hasReachedMinLength()||!this.minLengthFlushScheduled&&this.hasReachedMinLength())&&(this.minLengthFlushScheduled=!0,this.scheduleFlush())}hasReachedMinLength(){return this.minReplayLengthMs<=0?!0:Date.now()-this.startTime>=this.minReplayLengthMs}scheduleFlush(){if(this.isFlushing||this.events.length===0||this.flushScheduled)return;this.flushScheduled=!0;let e=()=>{this.flushScheduled=!1,this.flushTimeout=null,this.idleFlushId=null,this.flush()};if(typeof window<`u`&&`requestIdleCallback`in window){this.idleFlushId=window.requestIdleCallback(e,{timeout:2e3});return}this.flushTimeout=setTimeout(e,0)}handleUnload=()=>{this.clearScheduledFlush(),this.flush({lowLatency:!0})};handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.flushTimer&&=(clearInterval(this.flushTimer),null),this.clearScheduledFlush(),this.flush({lowLatency:!0})):document.visibilityState===`visible`&&this.started&&(this.flushTimer||=setInterval(()=>{this.scheduleFlush()},this.flushInterval))};clearScheduledFlush(){this.idleFlushId!==null&&typeof window<`u`&&`cancelIdleCallback`in window&&window.cancelIdleCallback(this.idleFlushId),this.idleFlushId=null,this.flushTimeout&&=(clearTimeout(this.flushTimeout),null),this.flushScheduled=!1}scheduleMinLengthFlush(){this.minReplayLengthMs<=0||(this.clearMinLengthFlushTimer(),this.minLengthFlushTimer=setTimeout(()=>{this.minLengthFlushTimer=null,this.events.length!==0&&(this.minLengthFlushScheduled=!0,this.scheduleFlush())},this.minReplayLengthMs))}clearMinLengthFlushTimer(){this.minLengthFlushTimer&&=(clearTimeout(this.minLengthFlushTimer),null)}clearPendingRetryTimer(){this.pendingRetryTimer&&=(clearTimeout(this.pendingRetryTimer),null)}queuePendingBatch(e){if(this.pendingBatches.length>=this.maxPendingBatches){this.debug&&console.warn(`[Replay] Pending batch buffer full, dropping batch ${e.batch.sequence}`);return}this.pendingBatches.push(e)}schedulePendingRetry(e){this.pendingRetryTimer||=setTimeout(()=>{this.pendingRetryTimer=null,this.processPendingBatches()},e)}async flush(e={}){if(this.events.length===0){this.processPendingBatches();return}if(!this.hasReachedMinLength()){this.debug&&console.log(`[Replay] Too short (${Date.now()-this.startTime}ms), skipping`),this.processPendingBatches();return}if(this.isFlushing)return;this.isFlushing=!0;let r=this.events;this.events=[];let i=t(),a={token:this.siteKey,sessionId:n(),...i?{identifier:i}:{},sequence:this.sequenceNumber++,timestamp:Date.now(),url:window.location.href,events:r};if(this.pendingBatches.length>0||this.isProcessingPending){this.queuePendingBatch({batch:a,isCompressed:this.compressionSupported&&!e.lowLatency,retries:0}),this.isFlushing=!1,this.processPendingBatches();return}this.debug&&console.log(`[Replay] Sending ${r.length} events (seq: ${a.sequence})`);try{let t,n=!1;if(this.compressionSupported&&!e.lowLatency)try{t=await this.compress(JSON.stringify(a)),n=!0}catch{this.debug&&console.warn(`[Replay] Compression failed, using uncompressed`),t=new Blob([JSON.stringify(a)],{type:`application/json`})}else t=new Blob([JSON.stringify(a)],{type:`application/json`});await this.send(t,n,{useBeacon:e.lowLatency===!0})||this.queuePendingBatch({batch:a,isCompressed:n,retries:0})}catch(e){this.debug&&console.warn(`[Replay] Flush error:`,e),this.queuePendingBatch({batch:a,isCompressed:!1,retries:0})}finally{this.isFlushing=!1,this.processPendingBatches()}}async processPendingBatches(){if(!(this.isProcessingPending||this.pendingBatches.length===0)){this.clearPendingRetryTimer(),this.isProcessingPending=!0;try{for(;this.pendingBatches.length>0;){let e=this.pendingBatches[0];if(!e)break;if(e.retries>=3){this.debug&&console.warn(`[Replay] Max retries reached, restoring batch ${e.batch.sequence} to buffer`),this.pendingBatches.shift(),this.events=[...e.batch.events,...this.events],this.scheduleFlush();continue}e.retries++;let t;if(e.isCompressed&&this.compressionSupported)try{t=await this.compress(JSON.stringify(e.batch))}catch{t=new Blob([JSON.stringify(e.batch)],{type:`application/json`}),e.isCompressed=!1}else t=new Blob([JSON.stringify(e.batch)],{type:`application/json`});if(await this.send(t,e.isCompressed,{useBeacon:!1})){this.pendingBatches.shift();continue}this.schedulePendingRetry(1e3*e.retries);break}}catch{let e=this.pendingBatches[0];this.debug&&e&&console.warn(`[Replay] Retry ${e.retries} failed`),this.schedulePendingRetry(e?1e3*e.retries:1e3)}finally{this.isProcessingPending=!1}}}async compress(e){if(!this.compressionSupported)throw Error(`Compression not supported`);let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();r.write(t),r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;if(this.debug){let e=(s.length/t.length*100).toFixed(1);console.log(`[Replay] Compressed: ${t.length} → ${s.length} bytes (${e}%)`)}return new Blob([s],{type:`application/octet-stream`})}async send(e,t,n={}){let r=t?`${this.endpoint}?encoding=gzip`:this.endpoint,a=(e.size/1024).toFixed(1);return i({url:r,data:e,contentType:t?`application/octet-stream`:`application/json`,debug:this.debug,debugPrefix:`[Replay] ${a}KB`,useBeacon:n.useBeacon??!1,keepalive:n.useBeacon===!0})??Promise.resolve(!1)}getSessionId(){return n()}};export{l as default};
@@ -1 +0,0 @@
1
- const e=`https://metrics.faststats.dev`;function t(e){return e.replace(/\/+$/,``)}function n(n){return n===void 0||n===``?e:t(n)||e}function r(e){return`${n(e)}/v1/web`}function i(e){return`${n(e)}/v1/identify`}function a(e){return`${n(e)}/v1/replay`}function o(e){return`${n(e)}/v1/vitals`}let s=!1;function c(e){s=e}function l(e){return e??s?``:u()}function u(){if(typeof localStorage>`u`)return``;let e=localStorage.getItem(`faststats_anon_id`);if(e)return e;let t=crypto.randomUUID();return localStorage.setItem(`faststats_anon_id`,t),t}function d(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),u())}function f(){if(typeof sessionStorage>`u`)return``;let e=sessionStorage.getItem(`session_id`),t=sessionStorage.getItem(`session_timestamp`);if(e&&t){if(Date.now()-Number.parseInt(t,10)<18e5)return sessionStorage.setItem(`session_timestamp`,Date.now().toString()),e;sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`)}let n=Date.now().toString(),r=crypto.randomUUID();return sessionStorage.setItem(`session_id`,r),sessionStorage.setItem(`session_timestamp`,n),sessionStorage.setItem(`session_start`,n),r}function p(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),f())}function m(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function h(){if(typeof sessionStorage>`u`)return Date.now();let e=sessionStorage.getItem(`session_start`);if(e)return Number.parseInt(e,10);let t=sessionStorage.getItem(`session_timestamp`);return t?Number.parseInt(t,10):Date.now()}function g(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{m as a,c,a as d,r as f,h as i,i as l,l as n,d as o,o as p,f as r,p as s,g as t,n as u};
@@ -1 +0,0 @@
1
- import{p as e,r as t,t as n}from"./types-8FXsUqbi.js";import{onCLS as r,onFCP as i,onINP as a,onLCP as o,onTTFB as s}from"web-vitals/attribution";var c=class{endpoint;siteKey;debug;samplingPercentage;started=!1;sessionSamplingSeed;metricsMap=new Map;flushed=!1;handleVisibilityChange=()=>{document.visibilityState===`hidden`&&this.finalizeAndFlush()};handlePageHide=()=>{this.finalizeAndFlush()};constructor(t){this.siteKey=t.siteKey,this.endpoint=e(t.baseUrl),this.debug=t.debug??!1,this.samplingPercentage=n(t.samplingPercentage),this.sessionSamplingSeed=Math.random()*100}start(){this.started||typeof window>`u`||(this.started=!0,this.flushed=!1,document.addEventListener(`visibilitychange`,this.handleVisibilityChange),window.addEventListener(`pagehide`,this.handlePageHide),this.debug&&console.log(`[WebVitals] Tracking started`),r(e=>this.captureMetric(e)),a(e=>this.captureMetric(e)),o(e=>this.captureMetric(e)),i(e=>this.captureMetric(e)),s(e=>this.captureMetric(e)))}stop(){!this.started||typeof window>`u`||(this.started=!1,document.removeEventListener(`visibilitychange`,this.handleVisibilityChange),window.removeEventListener(`pagehide`,this.handlePageHide),this.finalizeAndFlush())}captureMetric(e){if(this.flushed||this.samplingPercentage<100&&this.sessionSamplingSeed>=this.samplingPercentage)return;let t=e.name,n={id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}},r=t===`FCP`||t===`TTFB`;this.metricsMap.set(t,{value:e.value,attributes:n,final:r}),this.debug&&console.log(`[WebVitals] ${t} captured: ${e.value}`+(r?` (final)`:``))}finalizeAndFlush(){if(!(this.flushed||this.metricsMap.size===0)){for(let[e,t]of this.metricsMap.entries())t.final||(t.final=!0,this.debug&&console.log(`[WebVitals] ${e} finalized: ${t.value}`));this.flushWithBeacon()}}buildPayload(){if(this.metricsMap.size===0)return null;let e=Array.from(this.metricsMap.entries()).map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes}));return{body:JSON.stringify({sessionId:t(),vitals:e,metadata:{url:window.location.href}}),count:e.length}}flushWithBeacon(){let e=this.buildPayload();if(e){if(this.flushed=!0,this.debug){let t=Array.from(this.metricsMap.keys()).join(`, `);console.log(`[WebVitals] Sending final metrics (${e.count}): ${t}`)}typeof fetch==`function`&&fetch(this.endpoint,{method:`POST`,body:e.body,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.siteKey}`},keepalive:!0}).catch(()=>{this.debug&&console.warn(`[WebVitals] Failed to send metrics`)})}}};export{c as default};
@@ -1,24 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import ReplayTracker from "../src/replay";
3
-
4
- describe("ReplayTracker performance", () => {
5
- test("event ingestion stays within budget", () => {
6
- const tracker = new ReplayTracker({
7
- siteKey: "site_test",
8
- maxEvents: 20000,
9
- minReplayLengthMs: Number.MAX_SAFE_INTEGER,
10
- }) as unknown as {
11
- handleEvent: (event: unknown, isCheckout?: boolean) => void;
12
- events: unknown[];
13
- };
14
-
15
- const startedAt = performance.now();
16
- for (let i = 0; i < 10000; i++) {
17
- tracker.handleEvent({ type: 2, timestamp: i }, false);
18
- }
19
- const elapsed = performance.now() - startedAt;
20
-
21
- expect(tracker.events.length).toBe(10000);
22
- expect(elapsed).toBeLessThan(500);
23
- });
24
- });