@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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # @faststats/web
2
2
 
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 55032f8: feat: add feature flags
8
+
9
+ ### Patch Changes
10
+
11
+ - 98385da: refactor: reduce bundle size and make results more stable
12
+
3
13
  ## 0.1.9
4
14
 
5
15
  ### Patch Changes
@@ -0,0 +1 @@
1
+ import{a as e,c as t,d as n,f as r,i,l as a,m as o,n as s,o as c,r as l,s as u,t as d,u as f}from"./types-PBKNFqxj.js";function p(e){let t=JSON.stringify(e);if(typeof btoa!=`function`)return t;let n=new TextEncoder().encode(t),r=``;for(let e=0;e<n.length;e++){let t=n[e];t!==void 0&&(r+=String.fromCharCode(t))}return btoa(r)}async function m(e,t){let n=a(t.baseUrl,e),r=new Headers;if(t.projectToken&&t.projectId)throw Error(`provide either projectToken or projectId, not both`);if(t.serverId&&t.externalId)throw Error(`provide either serverId or externalId, not both`);let i=t.projectToken?[`X-Project-Token`,t.projectToken]:t.projectId?[`X-Project-Id`,t.projectId]:void 0,o=t.serverId?[`X-Server-Id`,t.serverId]:t.externalId?[`X-External-Id`,t.externalId]:void 0;if(!i)throw Error(`feature flag check requires projectToken or projectId`);if(!o)throw Error(`feature flag check requires serverId or externalId`);r.set(i[0],i[1]),r.set(o[0],o[1]),t.attributes&&Object.keys(t.attributes).length>0&&r.set(`X-Attributes`,p(t.attributes));let s=await fetch(n,{method:`GET`,headers:r,credentials:`omit`,signal:t.signal});if(!s.ok)throw Error(`feature flag check failed: HTTP ${s.status}`);let c=(await s.json()).evaluation;return c?.enabled===!0||c?.enabled===!1?c:{enabled:!1}}let h=null,g;function _(){return h}function v(e,t){typeof window>`u`||T()||h?.track(e,t??{})}function y(e,t,n){typeof window>`u`||T()||h?.identify(e,t,n??{})}function b(e=!0){typeof window>`u`||T()||h?.logout(e)}function x(e){if(h){h.setConsentMode(e);return}g=e}function S(){x(`granted`)}function C(){x(`denied`)}function w(e){typeof window>`u`||T()||h?.reportError(e)}function T(){if(typeof localStorage>`u`)return!1;let e=localStorage.getItem(`disable-faststats`);return e===`true`||e===`1`}async function E(e){let{url:t,data:n,contentType:r=`application/json`,headers:i={},debug:a=!1,debugPrefix:o=`[Analytics]`,useBeacon:s=!0,keepalive:c=!0}=e;if(s&&typeof 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 D(e){for(;e;){if(e instanceof HTMLAnchorElement&&e.href)return e;e=e.parentNode}return null}function O(){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 k=class{webEndpoint;baseUrl;featureFlagsBaseUrl;debug;started=!1;destroyed=!1;pageKey=``;navTimer=null;heartbeatTimer=null;scrollDepth=0;pageEntryTime=0;pagePath=``;pageUrl=``;pageHash=``;hasLeftCurrentPage=!1;scrollHandler=null;consentMode;cookielessWhilePending;cleanupCallbacks=[];childTrackers=[];pendingReportedErrors=[];errorTracker=null;handleVisibilityChange=()=>{document.visibilityState===`hidden`?(this.leavePage(),this.stopHeartbeat()):this.startHeartbeat()};handlePageHide=()=>{this.leavePage()};handlePopState=()=>{this.navigate()};handleHashChange=()=>{this.navigate()};constructor(e){this.options=e,this.baseUrl=n(e.baseUrl),this.featureFlagsBaseUrl=r(e.featureFlagsBaseUrl),this.webEndpoint=o(this.baseUrl),this.debug=e.debug??!1,this.consentMode=e.consent?.mode??`granted`,this.cookielessWhilePending=e.consent?.cookielessWhilePending??!0,g!==void 0&&(this.consentMode=g,g=void 0),(e.autoTrack??!0)&&this.init()}log(e){this.debug&&console.log(`[Analytics] ${e}`)}init(){if(!(typeof window>`u`)){if(T()){this.log(`disabled`);return}h=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||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}async start(){if(this.started||this.destroyed||typeof window>`u`)return;if(h&&h!==this){this.log(`already started by another instance`);return}if(T()){this.log(`disabled`);return}this.started=!0,h=this,t(this.isCookielessMode());let e=this.options;if(e.trackErrors)try{let{default:t}=await import(`./error-BWTalC2O.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-BAkrZEHv.js`),n=new t({siteKey:e.siteKey,baseUrl:this.baseUrl,debug:this.debug,samplingPercentage:d(e.webVitals?.sampling?.percentage)});n.start(),this.childTrackers.push(n),this.log(`web-vitals loaded`)}catch(e){this.log(`failed to initialize web-vitals tracker: ${String(e)}`)}if(e.trackReplay)try{let{default:t}=await import(`./replay-4Jtqp78w.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(),h===this&&(h=null),this.started=!1,this.destroyed=!0}}pageview(e={}){if(!this.ensureStarted())return;let t=`${location.pathname}|${this.options.trackHash??!1?location.hash:``}`;t!==this.pageKey&&(this.pageKey=t,this.send(`pageview`,e))}track(e,t={}){this.ensureStarted()&&this.send(e,t)}identify(e,t,n={}){if(!this.ensureStarted()||this.isCookielessMode())return;let r=e.trim(),i=t.trim();!r||!i||E({url:f(this.baseUrl),data:JSON.stringify({token:this.options.siteKey,identifier:s(!1),externalId:r,email:i,name:n.name?.trim()||void 0,phone:n.phone?.trim()||void 0,avatarUrl:n.avatarUrl?.trim()||void 0,traits:n.traits??{}}),contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] identify`})}logout(e=!0){this.ensureStarted()&&(e&&c(this.isCookielessMode()),u())}setConsentMode(e){this.consentMode=e,t(this.isCookielessMode())}optIn(){this.setConsentMode(`granted`)}optOut(){this.setConsentMode(`denied`)}getConsentMode(){return this.consentMode}getAnonymousId(){return s(this.isCookielessMode())}getSessionId(){return l()}async checkFeatureFlag(e,t,n){if(typeof window>`u`||T())return{enabled:!1};let r=n?.externalId?.trim();if(r)return m(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,externalId:r,attributes:t,signal:n?.signal});let i=this.getAnonymousId();return i?m(e,{baseUrl:this.featureFlagsBaseUrl,projectToken:this.options.siteKey,serverId:i,attributes:t,signal:n?.signal}):{enabled:!1}}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=s(this.isCookielessMode()),r=JSON.stringify({token:this.options.siteKey,...n?{userId:n}:{},sessionId:l(),data:{event:e,page:location.pathname,referrer:document.referrer||null,title:document.title||``,url:location.href,...O(),...t}});this.log(e),E({url:this.webEndpoint,data:r,contentType:`text/plain`,debug:this.debug,debugPrefix:`[Analytics] ${e}`})}enterPage(){this.pageEntryTime=Date.now(),this.pagePath=location.pathname,this.pageUrl=location.href,this.pageHash=location.hash,this.scrollDepth=0,this.hasLeftCurrentPage=!1}leavePage(){if(this.destroyed||this.hasLeftCurrentPage)return;this.hasLeftCurrentPage=!0;let e=Date.now();this.send(`page_leave`,{page:this.pagePath,url:this.pageUrl,time_on_page:e-this.pageEntryTime,scroll_depth:this.scrollDepth,session_duration:e-i()})}trackScroll(){this.scrollHandler&&window.removeEventListener(`scroll`,this.scrollHandler);let e=()=>{let e=document.documentElement,t=document.body,n=window.innerHeight,r=Math.max(e.scrollHeight,t.scrollHeight);if(r<=n){this.scrollDepth=100;return}let i=Math.min(100,Math.round(((window.scrollY||e.scrollTop)+n)/r*100));i>this.scrollDepth&&(this.scrollDepth=i)};this.scrollHandler=e,e(),window.addEventListener(`scroll`,e,{passive:!0})}startHeartbeat(){this.stopHeartbeat(),this.heartbeatTimer=setInterval(()=>{if(document.visibilityState===`hidden`){this.stopHeartbeat();return}e()},300*1e3)}navigate(){!this.started||this.destroyed||(this.stopNavigationTimer(),this.navTimer=setTimeout(()=>{this.navTimer=null;let e=location.pathname!==this.pagePath,t=(this.options.trackHash??!1)&&location.hash!==this.pageHash;!e&&!t||(this.leavePage(),this.enterPage(),this.trackScroll(),this.pageview({trigger:`navigation`}))},300))}links(){let e=e=>{let t=D(e.target);t&&t.host!==location.host&&this.track(`outbound_link`,{outbound_link:t.href})};this.addDocumentListener(`click`,e),this.addDocumentListener(`auxclick`,e)}};export{b as a,w as c,v as d,m as f,T as i,E as l,_ as n,S as o,y as r,C as s,k as t,x as u};
@@ -0,0 +1,2 @@
1
+ import{m as e,n as t,r as n}from"./types-PBKNFqxj.js";import{l as r}from"./analytics-6gDXSBjL.js";const i=/(?:^|\()(chrome|moz|ms-browser|safari-web)-extension:\/\//,a=e=>e?.split(`
2
+ `).map(e=>e.trim()).filter(Boolean);function o(e){if(e instanceof Error)return{error:e.name?.trim()||`Error`,message:e.message,stack:a(e.stack),cause:o(e.cause)};if(typeof e==`string`)return{error:`Error`,message:e}}function s(e){return e?`${e.error}\0${e.message??``}\0${s(e.cause)}`:``}function c(e){let t=[e.kind,e.handled?`handled`:`unhandled`,e.message,e.filename??``,e.lineno??``,s(e.cause)].join(`\0`),n=2166136261,r=3598710387;for(let e=0;e<t.length;e++){let i=t.charCodeAt(e);n=Math.imul(n^i,16777619),r=Math.imul(r^i,2246822519)}return`err_${(n>>>0).toString(16).padStart(8,`0`)}${(r>>>0).toString(16).padStart(8,`0`)}`}var l=class{endpoint;handled=new WeakSet;queue=new Map;timer=null;started=!1;constructor(t){this.options=t,this.endpoint=e(t.baseUrl)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??5e3}get maxQueueSize(){return this.options.maxQueueSize??50}log(...e){this.debug&&console.log(`[ErrorTracker]`,...e)}start(){this.started||typeof window>`u`||(this.started=!0,window.addEventListener(`error`,this.onError),window.addEventListener(`unhandledrejection`,this.onRejection),document.addEventListener(`visibilitychange`,this.onVisibilityChange),window.addEventListener(`pagehide`,this.flush),this.timer=setInterval(this.flush,this.flushInterval),this.log(`Started listening for errors`))}stop(){!this.started||typeof window>`u`||(this.started=!1,window.removeEventListener(`error`,this.onError),window.removeEventListener(`unhandledrejection`,this.onRejection),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),window.removeEventListener(`pagehide`,this.flush),this.timer&&clearInterval(this.timer),this.timer=null,this.flush(),this.log(`Stopped listening for errors`))}captureError(e){this.record({kind:`error`,message:e.message,stack:e.stack,handled:!0,cause:o(e.cause)},e)}onError=e=>{let t=e.error;this.record({kind:`error`,message:e.message||`Unknown error`,filename:e.filename||void 0,lineno:e.lineno||void 0,stack:t instanceof Error?t.stack:void 0,handled:!1,cause:t instanceof Error?o(t.cause):void 0},t)};onRejection=e=>{let t=e.reason;this.record({kind:`unhandledrejection`,message:t.message||`Unhandled promise rejection`,stack:t.stack||void 0,handled:!1,cause:o(t.cause)},t)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush()};record(e,t){if(t instanceof Error){if(this.handled.has(t)){this.log(`Skipping duplicate:`,t.message);return}this.handled.add(t)}if(i.test(`${e.filename??``}\n${e.stack??``}`))return;let n=c(e),r=this.queue.get(n);r?r.count+=1:this.queue.set(n,{hash:n,count:1,error:e.kind===`unhandledrejection`?`UnhandledRejection`:`Error`,message:e.message,stack:a(e.stack),handled:e.handled,...e.cause?{cause:e.cause}:{}}),this.log(`Captured error:`,e),this.queue.size>=this.maxQueueSize&&this.flush()}flush=()=>{if(this.queue.size===0)return;let e=[...this.queue.values()];this.queue.clear();let i=t(),a=globalThis.__SOURCEMAPS_BUILD__,o=typeof a?.buildId==`string`&&a.buildId.trim()?a.buildId:void 0,s=JSON.stringify({token:this.options.siteKey,...i?{userId:i}:{},sessionId:n(),...o?{buildId:o}:{},data:{url:location.href,page:location.pathname,referrer:document.referrer||null,title:document.title},errors:e});this.log(`Flushing errors:`,e),this.log(`Payload:`,s),r({url:this.endpoint,data:s,debug:this.debug,debugPrefix:`[ErrorTracker]`})}};export{l as default};
package/dist/module.d.ts CHANGED
@@ -1,6 +1,24 @@
1
1
  import { recordOptions } from "rrweb/typings/types";
2
2
  import { SlimDOMOptions } from "rrweb-snapshot";
3
3
 
4
+ //#region src/feature-flags.d.ts
5
+ type FeatureFlagEvaluation = {
6
+ enabled: boolean;
7
+ value?: string;
8
+ variantKey?: string;
9
+ variantValue?: string;
10
+ };
11
+ type FeatureFlagCheckContext = {
12
+ baseUrl?: string;
13
+ projectToken?: string;
14
+ projectId?: string;
15
+ serverId?: string;
16
+ externalId?: string;
17
+ attributes?: Record<string, unknown>;
18
+ signal?: AbortSignal;
19
+ };
20
+ declare function fetchFeatureFlagEvaluation(flagKey: string, context: FeatureFlagCheckContext): Promise<FeatureFlagEvaluation>;
21
+ //#endregion
4
22
  //#region ../../node_modules/.bun/@rrweb+types@2.0.0-alpha.20/node_modules/@rrweb/types/dist/index.d.ts
5
23
  declare type addedNodeMutation = {
6
24
  parentId: number;
@@ -412,6 +430,7 @@ interface IdentifyOptions {
412
430
  interface WebAnalyticsOptions {
413
431
  siteKey: string;
414
432
  baseUrl?: string;
433
+ featureFlagsBaseUrl?: string;
415
434
  debug?: boolean;
416
435
  autoTrack?: boolean;
417
436
  trackHash?: boolean;
@@ -438,6 +457,7 @@ declare class WebAnalytics {
438
457
  private readonly options;
439
458
  private readonly webEndpoint;
440
459
  private readonly baseUrl;
460
+ private readonly featureFlagsBaseUrl;
441
461
  private readonly debug;
442
462
  private started;
443
463
  private destroyed;
@@ -472,7 +492,6 @@ declare class WebAnalytics {
472
492
  private stopHeartbeat;
473
493
  private stopNavigationTimer;
474
494
  private stopChildTrackers;
475
- private getErrorTracker;
476
495
  start(): Promise<void>;
477
496
  destroy(): void;
478
497
  pageview(extra?: Dict): void;
@@ -485,6 +504,10 @@ declare class WebAnalytics {
485
504
  getConsentMode(): ConsentMode;
486
505
  getAnonymousId(): string;
487
506
  getSessionId(): string;
507
+ checkFeatureFlag(key: string, attributes?: Record<string, unknown>, opts?: {
508
+ externalId?: string;
509
+ signal?: AbortSignal;
510
+ }): Promise<FeatureFlagEvaluation>;
488
511
  reportError(error: Error): void;
489
512
  private isCookielessMode;
490
513
  private send;
@@ -496,4 +519,8 @@ declare class WebAnalytics {
496
519
  private links;
497
520
  }
498
521
  //#endregion
499
- export { type ConsentMode, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions, getInstance, identify, isTrackingDisabled, logout, optIn, optOut, reportError, sendData, setConsentMode, trackEvent };
522
+ //#region src/utils/api-urls.d.ts
523
+ declare function normalizeFeatureFlagsBaseUrl(input?: string): string;
524
+ declare function featureFlagsCheckUrl(baseUrl: string | undefined, flagKey: string): string;
525
+ //#endregion
526
+ export { type ConsentMode, type FeatureFlagCheckContext, type FeatureFlagEvaluation, type IdentifyOptions, WebAnalytics, type WebAnalyticsOptions, featureFlagsCheckUrl, fetchFeatureFlagEvaluation, getInstance, identify, isTrackingDisabled, logout, normalizeFeatureFlagsBaseUrl, optIn, optOut, reportError, sendData, setConsentMode, trackEvent };
package/dist/module.js CHANGED
@@ -1 +1 @@
1
- import"./types-8FXsUqbi.js";import{a as e,c as t,d as n,i as r,l as i,n as a,o,r as s,s as c,t as l,u}from"./analytics-B1kgY_Yf.js";export{l as WebAnalytics,a as getInstance,s as identify,r as isTrackingDisabled,e as logout,o as optIn,c as optOut,t as reportError,i as sendData,u as setConsentMode,n as trackEvent};
1
+ import{f as e,l as t}from"./types-PBKNFqxj.js";import{a as n,c as r,d as i,f as a,i as o,l as s,n as c,o as l,r as u,s as d,t as f,u as p}from"./analytics-6gDXSBjL.js";export{f as WebAnalytics,t as featureFlagsCheckUrl,a as fetchFeatureFlagEvaluation,c as getInstance,u as identify,o as isTrackingDisabled,n as logout,e as normalizeFeatureFlagsBaseUrl,l as optIn,d as optOut,r as reportError,s as sendData,p as setConsentMode,i as trackEvent};
@@ -0,0 +1 @@
1
+ import{n as e,p as t,r as n,t as r}from"./types-PBKNFqxj.js";import{l as i}from"./analytics-6gDXSBjL.js";import{getRecordConsolePlugin as a}from"@rrweb/rrweb-plugin-console-record";import{getRecordSequentialIdPlugin as o}from"@rrweb/rrweb-plugin-sequential-id-record";import{EventType as s,record as c}from"rrweb";const l={mousemove:50,mouseInteraction:!0,scroll:150,media:800,input:`last`},u={script:!0,comment:!0,headFavicon:!0,headWhitespace:!0,headMetaDescKeywords:!0,headMetaSocial:!0,headMetaRobots:!0,headMetaHttpEquiv:!0,headMetaAuthorship:!0};var d=class{endpoint;compressionSupported=typeof window<`u`&&`CompressionStream`in window;sampled;events=[];pending=[];started=!1;startTime=0;sequence=0;intervalId=null;flushTask=null;retryTask=null;stopRecording;sending=!1;constructor(e){this.options=e,this.endpoint=t(e.baseUrl),this.sampled=Math.random()*100<r(e.samplingPercentage)}get debug(){return this.options.debug??!1}get flushInterval(){return this.options.flushInterval??1e4}get maxEvents(){return this.options.maxEvents??500}get maxPendingBatches(){return this.options.maxPendingBatches??30}get minReplayLengthMs(){return this.options.minReplayLengthMs??3e3}log(...e){this.debug&&console.log(`[Replay]`,...e)}start(){this.started||typeof window>`u`||!this.sampled||(this.started=!0,this.startTime=Date.now(),this.stopRecording=c({emit:this.onEvent,sampling:this.options.sampling??l,slimDOMOptions:this.options.slimDOMOptions??u,maskAllInputs:this.options.maskAllInputs??!0,maskInputOptions:this.options.maskInputOptions??{password:!0,email:!0,tel:!0},blockClass:this.options.blockClass,blockSelector:this.options.blockSelector,maskTextClass:this.options.maskTextClass,maskTextSelector:this.options.maskTextSelector,checkoutEveryNms:this.options.checkoutEveryNms??6e4,checkoutEveryNth:this.options.checkoutEveryNth,plugins:[o({key:`_faststatsSeqId`}),...this.options.recordConsole??!0?[a()]:[]]}),this.intervalId=setInterval(this.requestFlush,this.flushInterval),window.addEventListener(`beforeunload`,this.onUnload),window.addEventListener(`pagehide`,this.onUnload),document.addEventListener(`visibilitychange`,this.onVisibilityChange),this.log(`Recording started`))}stop(){if(this.started){if(this.started=!1,this.stopRecording?.(),this.stopRecording=void 0,this.intervalId&&clearInterval(this.intervalId),this.flushTask&&clearTimeout(this.flushTask),this.retryTask&&clearTimeout(this.retryTask),this.intervalId=null,this.flushTask=null,this.retryTask=null,window.removeEventListener(`beforeunload`,this.onUnload),window.removeEventListener(`pagehide`,this.onUnload),document.removeEventListener(`visibilitychange`,this.onVisibilityChange),!this.hasReachedMinLength()){this.events.length=0,this.log(`Session too short (${Date.now()-this.startTime}ms), discarding events`);return}this.flush(!0),this.log(`Recording stopped`)}}getSessionId(){return n()}onEvent=(e,t)=>{this.events.push(e),(t||this.events.length>=this.maxEvents||e.type===s.FullSnapshot&&this.hasReachedMinLength())&&this.requestFlush()};onUnload=()=>{this.flush(!0)};onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush(!0)};hasReachedMinLength(){return this.minReplayLengthMs<=0||Date.now()-this.startTime>=this.minReplayLengthMs}requestFlush=()=>{this.flushTask||this.sending||this.events.length===0||(this.flushTask=setTimeout(()=>{this.flushTask=null,this.flush(!1)},0))};createBatch(t){let r=e();return{token:this.options.siteKey,sessionId:n(),...r?{identifier:r}:{},sequence:this.sequence++,timestamp:Date.now(),url:window.location.href,events:t}}async encodeBatch(e,t){let n=JSON.stringify(e);if(!this.compressionSupported||t)return{data:new Blob([n],{type:`application/json`}),isCompressed:!1};try{return{data:await this.compress(n),isCompressed:!0}}catch{return this.log(`Compression failed, using uncompressed`),{data:new Blob([n],{type:`application/json`}),isCompressed:!1}}}async flush(e){if(!this.sending&&(this.events.length>0&&(this.hasReachedMinLength()?this.pending.push(this.createBatch(this.events.splice(0))):this.log(`Too short (${Date.now()-this.startTime}ms), skipping flush`)),this.pending.length!==0)){this.sending=!0;try{for(;this.pending.length>0;){let t=this.pending[0];if(!t)break;let n=await this.encodeBatch(t,e);if(!await this.send(n.data,n.isCompressed,e)){this.pending.length>=this.maxPendingBatches&&(this.log(`Pending buffer full, dropping batch ${t.sequence}`),this.pending.shift()),this.scheduleRetry();break}this.log(`Sent ${t.events.length} events (seq: ${t.sequence})`),this.pending.shift(),e=!1}}finally{this.sending=!1}}}scheduleRetry(){this.retryTask||=setTimeout(()=>{this.retryTask=null,this.flush(!1)},1e3)}async compress(e){let t=new TextEncoder().encode(e),n=new CompressionStream(`gzip`),r=n.writable.getWriter();await r.write(t),await r.close();let i=[],a=n.readable.getReader();for(;;){let{done:e,value:t}=await a.read();if(e)break;t&&i.push(t)}let o=i.reduce((e,t)=>e+t.length,0),s=new Uint8Array(o),c=0;for(let e of i)s.set(e,c),c+=e.length;return this.log(`Compressed: ${t.length} → ${s.length} bytes (${(s.length/t.length*100).toFixed(1)}%)`),new Blob([s],{type:`application/octet-stream`})}send(e,t,n){return i({url:t?`${this.endpoint}?encoding=gzip`:this.endpoint,data:e,contentType:t?`application/octet-stream`:`application/json`,debug:this.debug,debugPrefix:`[Replay] ${(e.size/1024).toFixed(1)}KB`,useBeacon:n,keepalive:n})??Promise.resolve(!1)}};export{d as default};
@@ -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}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,t){return`${c(e)}/v1/check/${encodeURIComponent(t)}`}let u=!1;function d(e){u=e}function f(e){return e??u?``:p()}function p(){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 m(e){return e||typeof localStorage>`u`?``:(localStorage.removeItem(`faststats_anon_id`),p())}function h(){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 g(){return typeof sessionStorage>`u`?``:(sessionStorage.removeItem(`session_id`),sessionStorage.removeItem(`session_timestamp`),sessionStorage.removeItem(`session_start`),h())}function _(){typeof sessionStorage>`u`||sessionStorage.getItem(`session_id`)&&sessionStorage.setItem(`session_timestamp`,Date.now().toString())}function v(){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 y(e){return typeof e!=`number`||!Number.isFinite(e)?100:Math.max(0,Math.min(100,e))}export{_ as a,d as c,r as d,c as f,s as h,v as i,l,i as m,f as n,m as o,o as p,h as r,g as s,y as t,a as u};
@@ -0,0 +1 @@
1
+ import{h as e,r as t,t as n}from"./types-PBKNFqxj.js";import{onCLS as r,onFCP as i,onINP as a,onLCP as o,onTTFB as s}from"web-vitals/attribution";const c=[r,a,o,i,s];var l=class{endpoint;metrics=new Map;sampled;started=!1;flushed=!1;constructor(t){this.options=t,this.endpoint=e(t.baseUrl),this.sampled=Math.random()*100<n(t.samplingPercentage)}get debug(){return this.options.debug??!1}log(...e){this.debug&&console.log(`[WebVitals]`,...e)}start(){if(!(this.started||typeof window>`u`)){this.started=!0,this.flushed=!1,document.addEventListener(`visibilitychange`,this.onVisibilityChange),window.addEventListener(`pagehide`,this.flush),this.log(`Tracking started`);for(let e of c)e(this.captureMetric)}}stop(){!this.started||typeof window>`u`||(this.started=!1,document.removeEventListener(`visibilitychange`,this.onVisibilityChange),window.removeEventListener(`pagehide`,this.flush),this.flush())}onVisibilityChange=()=>{document.visibilityState===`hidden`&&this.flush()};captureMetric=e=>{if(this.flushed||!this.sampled)return;let t=e.name;this.metrics.set(t,{value:e.value,attributes:{id:e.id,rating:e.rating,delta:e.delta,navigationType:e.navigationType,...e.attribution??{}}}),this.log(`${t} captured: ${e.value}`)};flush=()=>{if(this.flushed||this.metrics.size===0)return;this.flushed=!0;let e=[...this.metrics.entries()].map(([e,t])=>({metric:e,value:t.value,attributes:t.attributes})),n=e.map(e=>e.metric).join(`, `);this.metrics.clear();let r=JSON.stringify({sessionId:t(),vitals:e,metadata:{url:window.location.href}});this.log(`Sending final metrics (${e.length}): ${n}`),typeof fetch==`function`&&fetch(this.endpoint,{method:`POST`,body:r,headers:{"Content-Type":`application/json`,Authorization:`Bearer ${this.options.siteKey}`},keepalive:!0}).catch(()=>{this.log(`Failed to send metrics`)})}};export{l as default};
package/package.json CHANGED
@@ -19,14 +19,12 @@
19
19
  "publishConfig": {
20
20
  "access": "public"
21
21
  },
22
- "version": "0.1.9",
22
+ "version": "0.2.0",
23
23
  "scripts": {
24
24
  "build": "tsdown",
25
25
  "dev": "bun run build",
26
26
  "typecheck": "tsc --noEmit -p tsconfig.json",
27
27
  "test": "bun test",
28
- "test:perf": "bun test tests/replay-stress.test.ts",
29
- "check:bundle-size": "node scripts/check-bundle-size.mjs",
30
28
  "deploy:worker": "wrangler deploy"
31
29
  },
32
30
  "devDependencies": {
package/src/analytics.ts CHANGED
@@ -1,9 +1,14 @@
1
1
  import type ErrorTracker from "./error";
2
+ import {
3
+ type FeatureFlagEvaluation,
4
+ fetchFeatureFlagEvaluation,
5
+ } from "./feature-flags";
2
6
  import type ReplayTracker from "./replay";
3
7
  import type { ReplayTrackerOptions } from "./replay";
4
8
  import {
5
9
  identifyEventsUrl,
6
10
  normalizeAnalyticsBaseUrl,
11
+ normalizeFeatureFlagsBaseUrl,
7
12
  webEventsUrl,
8
13
  } from "./utils/api-urls";
9
14
  import {
@@ -59,6 +64,7 @@ export interface IdentifyOptions {
59
64
  export interface WebAnalyticsOptions {
60
65
  siteKey: string;
61
66
  baseUrl?: string;
67
+ featureFlagsBaseUrl?: string;
62
68
  debug?: boolean;
63
69
  autoTrack?: boolean;
64
70
  trackHash?: boolean;
@@ -223,6 +229,7 @@ function getUTM(): Record<string, string> {
223
229
  export class WebAnalytics {
224
230
  private readonly webEndpoint: string;
225
231
  private readonly baseUrl: string;
232
+ private readonly featureFlagsBaseUrl: string;
226
233
  private readonly debug: boolean;
227
234
  private started = false;
228
235
  private destroyed = false;
@@ -262,6 +269,9 @@ export class WebAnalytics {
262
269
 
263
270
  constructor(private readonly options: WebAnalyticsOptions) {
264
271
  this.baseUrl = normalizeAnalyticsBaseUrl(options.baseUrl);
272
+ this.featureFlagsBaseUrl = normalizeFeatureFlagsBaseUrl(
273
+ options.featureFlagsBaseUrl,
274
+ );
265
275
  this.webEndpoint = webEventsUrl(this.baseUrl);
266
276
  this.debug = options.debug ?? false;
267
277
  this.consentMode = options.consent?.mode ?? "granted";
@@ -363,10 +373,6 @@ export class WebAnalytics {
363
373
  this.errorTracker = null;
364
374
  }
365
375
 
366
- private getErrorTracker(): ErrorTracker | null {
367
- return this.errorTracker;
368
- }
369
-
370
376
  async start(): Promise<void> {
371
377
  if (this.started || this.destroyed || typeof window === "undefined") return;
372
378
  if (_instance && _instance !== this) {
@@ -561,6 +567,37 @@ export class WebAnalytics {
561
567
  return getOrCreateSessionId();
562
568
  }
563
569
 
570
+ async checkFeatureFlag(
571
+ key: string,
572
+ attributes?: Record<string, unknown>,
573
+ opts?: { externalId?: string; signal?: AbortSignal },
574
+ ): Promise<FeatureFlagEvaluation> {
575
+ if (typeof window === "undefined" || isTrackingDisabled()) {
576
+ return { enabled: false };
577
+ }
578
+ const externalId = opts?.externalId?.trim();
579
+ if (externalId) {
580
+ return fetchFeatureFlagEvaluation(key, {
581
+ baseUrl: this.featureFlagsBaseUrl,
582
+ projectToken: this.options.siteKey,
583
+ externalId,
584
+ attributes,
585
+ signal: opts?.signal,
586
+ });
587
+ }
588
+ const serverId = this.getAnonymousId();
589
+ if (!serverId) {
590
+ return { enabled: false };
591
+ }
592
+ return fetchFeatureFlagEvaluation(key, {
593
+ baseUrl: this.featureFlagsBaseUrl,
594
+ projectToken: this.options.siteKey,
595
+ serverId,
596
+ attributes,
597
+ signal: opts?.signal,
598
+ });
599
+ }
600
+
564
601
  reportError(error: Error): void {
565
602
  if (
566
603
  this.destroyed ||
@@ -571,7 +608,7 @@ export class WebAnalytics {
571
608
  }
572
609
  if (!(this.options.trackErrors ?? false)) return;
573
610
  if (!this.ensureStarted()) return;
574
- const tracker = this.getErrorTracker();
611
+ const tracker = this.errorTracker;
575
612
  if (tracker) {
576
613
  tracker.captureError(error);
577
614
  return;