UNPKG

abmeter

Version:

ABMeter browser SDK — feature flags and A/B experiments with server-side pre-evaluated assignments

2 lines (1 loc) 11.8 kB
"use strict";var abmeter=(()=>{var f=Object.defineProperty;var Q=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var V=Object.prototype.hasOwnProperty;var X=(r,e)=>{for(var t in e)f(r,t,{get:e[t],enumerable:!0})},z=(r,e,t,s)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of q(e))!V.call(r,n)&&n!==t&&f(r,n,{get:()=>e[n],enumerable:!(s=Q(e,n))||s.enumerable});return r};var Y=r=>z(f({},"__esModule",{value:!0}),r);var pe={};X(pe,{ApiError:()=>a,VERSION:()=>F,configure:()=>M,flush:()=>B,getExposure:()=>L,ready:()=>O,reset:()=>H,resolveParameter:()=>$,trackEvent:()=>D});var w="https://abmeter.ai";var I=Math.floor(52428.8),k="abmeter:assignments:",y="abmeter:track_id",v="abmeter_track_id",A=365*24*60*60,C="/api/v1/user-assignments",b="/api/v1/exposures",x="/api/v1/events";var c=class{constructor({http:e,user:t,logger:s}){this.assignments={};this.http=e,this.user=t,this.logger=s,this.storage=G()}hydrateFromStorage(){let e=this.storageRead(this.storageKey());if(!e)return!1;try{let t=JSON.parse(e);return t?.payload?.assignments?(this.assignments=t.payload.assignments,this.etag=t.etag,!0):!1}catch{return!1}}async refresh(){let e=this.user.email===void 0?{user_id:this.user.userId}:{user_id:this.user.userId,email:this.user.email},t=await this.http.fetchAssignments(e,this.etag);t.status!==304&&t.payload&&(this.assignments=t.payload.assignments,this.etag=t.etag,this.persist({etag:t.etag,payload:t.payload}))}resolveValue(e){return this.assignments[e]?.value}resolveAssignment(e){return this.assignments[e]}has(e){return e in this.assignments}persist(e){try{this.storage?.setItem(this.storageKey(),JSON.stringify(e))}catch(t){this.logger("assignments cache not persisted",t)}}storageRead(e){try{return this.storage?.getItem(e)??null}catch{return null}}storageKey(){return`${k}${this.user.userId}`}};function G(){try{let r=globalThis.localStorage,e="abmeter:probe";return r.setItem(e,"1"),r.removeItem(e),r}catch{return null}}var a=class extends Error{constructor(e,t){let s=typeof t=="object"&&t!==null?t:{};super(typeof s.error=="string"?s.error:`HTTP ${e}`),this.name="ApiError",this.status=e,this.details=s.details}get retryable(){return this.status>=500||this.status===408||this.status===429}get partialFailure(){return this.status===400&&Array.isArray(this.details?.failures)&&this.details.failures.length>0}};var g=class{constructor(e){this.exposures=[];this.events=[];this.retryQueue=[];this.timer=null;this.flushChain=Promise.resolve();this.queuedFlush=null;this.onVisibilityChange=()=>{globalThis.document?.visibilityState==="hidden"&&this.drainOnHide()};this.onPageHide=()=>{this.drainOnHide()};this.http=e.http,this.flushIntervalMs=e.flushIntervalMs,this.batchSize=e.batchSize??20,this.logger=e.logger,this.errorCallback=e.errorCallback}start(){this.timer===null&&(this.timer=setInterval(()=>{this.flush()},this.flushIntervalMs)),globalThis.document?.addEventListener("visibilitychange",this.onVisibilityChange),globalThis.window?.addEventListener("pagehide",this.onPageHide)}stop(){this.timer!==null&&(clearInterval(this.timer),this.timer=null),globalThis.document?.removeEventListener("visibilitychange",this.onVisibilityChange),globalThis.window?.removeEventListener("pagehide",this.onPageHide)}queueExposure(e){this.exposures.push({kind:"exposure",data:e,attempts:0}),this.flushIfFull()}queueEvent(e){this.events.push({kind:"event",data:e,attempts:0}),this.flushIfFull()}pending(){return this.exposures.length+this.events.length+this.retryQueue.length}flush(){if(this.queuedFlush)return this.queuedFlush;let e=this.flushChain.then(()=>(this.queuedFlush=null,this.drain()));return this.queuedFlush=e,this.flushChain=e.catch(()=>{}),e}async drain(){for(await this.retryOneBatch();this.exposures.length>0||this.events.length>0;)this.exposures.length>0&&await this.submitBatch("exposure",this.exposures.splice(0,this.batchSize)),this.events.length>0&&await this.submitBatch("event",this.events.splice(0,this.batchSize))}async reset(e={}){if(this.stop(),e.force){let t=this.pending();this.exposures=[],this.events=[],this.retryQueue=[],t>0&&this.logger(`reset(force): dropped ${t} queued items`);return}for(;this.pending()>0;)await this.flush()}drainOnHide(){let e=[...this.retryQueue,...this.exposures,...this.events];this.retryQueue=[],this.exposures=[],this.events=[];for(let t of["exposure","event"]){let s=e.filter(n=>n.kind===t).map(n=>n.data);for(let n of Z(s,this.batchSize))this.sendChunkOnHide(t,n)}}sendChunkOnHide(e,t){let s=()=>{(e==="exposure"?this.http.beaconExposures(t):this.http.beaconEvents(t))||this.logger(`beacon refused; dropping ${t.length} ${e}s`)};try{(e==="exposure"?this.http.postExposures(t,{keepalive:!0}):this.http.postEvents(t,{keepalive:!0})).catch(s)}catch{s()}}flushIfFull(){this.exposures.length+this.events.length>=this.batchSize&&this.flush()}async retryOneBatch(){if(this.retryQueue.length===0)return;let e=this.retryQueue.splice(0,this.batchSize),t=new Map;for(let s of e){let n=t.get(s.kind)??[];n.push(s),t.set(s.kind,n)}for(let[s,n]of t)await this.submitBatch(s,n)}async submitBatch(e,t){let s=t.map(n=>n.data);try{e==="exposure"?await this.http.postExposures(s):await this.http.postEvents(s)}catch(n){this.handleSubmitError(e,t,n)}}handleSubmitError(e,t,s){s instanceof a?s.retryable?(this.logger(`retryable API error for ${t.length} ${e}s`,s.message),this.requeue(t)):s.partialFailure?this.logger(`partial failure, dropping batch of ${t.length} ${e}s`,s.message):this.logger(`permanent API error, dropping ${t.length} ${e}s`,s.message):e==="exposure"?(this.logger(`network error, re-queueing ${t.length} exposures`,s),this.requeue(t)):this.logger(`network error, dropping ${t.length} events`,s),this.errorCallback?.(s)}requeue(e){for(let t of e)t.attempts+=1,t.attempts>=3?this.logger(`max retries exceeded, dropping ${t.kind}`):this.retryQueue.length>=1e3?this.logger(`retry queue full, dropping ${t.kind}`):this.retryQueue.push(t)}};function Z(r,e){let t=[],s=[],n=2;for(let o of r){let p=JSON.stringify(o).length+1;(n+p>I||s.length>=e)&&s.length>0&&(t.push(s),s=[],n=2),s.push(o),n+=p}return s.length>0&&t.push(s),t}var d=class{constructor({windowMs:e=6e5,maxKeys:t=1e3,now:s=Date.now}={}){this.entries=new Map;this.windowMs=e,this.maxKeys=t,this.now=s}seenRecently(e){let t=this.now(),s=this.entries.get(e),n=s!==void 0&&t-s<this.windowMs;return this.entries.delete(e),this.entries.set(e,n?s:t),this.evict(),n}evict(){for(;this.entries.size>this.maxKeys;){let e=this.entries.keys().next().value;if(e===void 0)return;this.entries.delete(e)}}};function P(r){return`${r.user_id}|${r.exposable_id}|${r.audience_id}|${JSON.stringify(r.resolved_value)}`}var h=class{constructor({baseUrl:e,apiKey:t}){this.baseUrl=e,this.apiKey=t}async fetchAssignments(e,t){let s={Authorization:`Bearer ${this.apiKey}`,"Content-Type":"application/json"};t&&(s["If-None-Match"]=t);let n=await fetch(`${this.baseUrl}${C}`,{method:"POST",headers:s,body:JSON.stringify(e)});if(n.status===304)return{status:304};if(!n.ok)throw new a(n.status,await T(n));return{status:200,payload:await n.json(),etag:n.headers.get("ETag")??void 0}}async postExposures(e,t){await this.post(b,e,t)}async postEvents(e,t){await this.post(x,e,t)}beaconExposures(e){return this.beacon(b,e)}beaconEvents(e){return this.beacon(x,e)}async post(e,t,s){let n=s?.keepalive??!1,o=await fetch(`${this.baseUrl}${e}`,{method:"POST",headers:{Authorization:`Bearer ${this.apiKey}`,"Content-Type":n?"text/plain":"application/json"},body:JSON.stringify(t),keepalive:n});if(!o.ok)throw new a(o.status,await T(o))}beacon(e,t){let s=globalThis.navigator?.sendBeacon?.bind(globalThis.navigator);if(!s)return!1;let n=`${this.baseUrl}${e}?api_key=${encodeURIComponent(this.apiKey)}`,o=new Blob([JSON.stringify(t)],{type:"text/plain"});try{return s(n,o)}catch{return!1}}};async function T(r){try{return await r.json()}catch{return}}function R(r){let e=r?.userId??ee();return r?.email===void 0?{userId:e}:{userId:e,email:r.email}}function ee(){let e=se(v)??ie(y)??re();return te(e),e}function te(r){ne(v,r),oe(y,r)}function re(){let r=globalThis.crypto;if(r?.randomUUID)return r.randomUUID();let e=new Uint8Array(16);if(r?.getRandomValues)r.getRandomValues(e);else for(let s=0;s<e.length;s+=1)e[s]=Math.floor(Math.random()*256);e[6]=e[6]&15|64,e[8]=e[8]&63|128;let t=Array.from(e,s=>s.toString(16).padStart(2,"0")).join("");return`${t.slice(0,8)}-${t.slice(8,12)}-${t.slice(12,16)}-${t.slice(16,20)}-${t.slice(20)}`}function se(r){if(typeof document>"u")return null;let e=document.cookie.split(";").map(t=>t.trim()).find(t=>t.startsWith(`${r}=`));return e&&decodeURIComponent(e.slice(r.length+1))||null}function ne(r,e){if(!(typeof document>"u"))try{document.cookie=`${r}=${encodeURIComponent(e)}; path=/; max-age=${A}; SameSite=Lax`}catch{}}function ie(r){try{return globalThis.localStorage?.getItem(r)??null}catch{return null}}function oe(r,e){try{globalThis.localStorage?.setItem(r,e)}catch{}}function m(r,e,t,s){try{return s()}catch(n){return U(r,n,t),e}}async function E(r,e,t,s){try{return await s()}catch(n){return U(r,n,t),e}}function U(r,e,t){t.logger?t.logger(`${r} failed`,e):console.error(`[abmeter] ${r} failed`,e);try{t.errorCallback?.(e)}catch{}}var ue="pk_";function K(r){if(!r||typeof r.apiKey!="string"||r.apiKey.length===0)throw new Error("abmeter.configure: apiKey is required");if(!r.apiKey.startsWith(ue))throw new Error("abmeter.configure: apiKey must be a publishable key (pk_...) \u2014 mint one on the Lab API Keys page. Secret keys (api-...) must never be shipped in browser code: anyone can read them from your page source and gain full access to your ABMeter account.");return{apiKey:r.apiKey,baseUrl:(r.baseUrl??w).replace(/\/+$/,""),flushIntervalMs:r.flushInterval??1e3,logger:r.logger??le,errorCallback:r.errorCallback}}function le(r,e){e===void 0?console.warn(`[abmeter] ${r}`):console.warn(`[abmeter] ${r}`,e)}var i=null;function M(r){let e=K(r),t=R(r.user),s=new h({baseUrl:e.baseUrl,apiKey:e.apiKey}),n=new c({http:s,user:t,logger:e.logger}),o=new g({http:s,flushIntervalMs:e.flushIntervalMs,logger:e.logger,errorCallback:e.errorCallback});if(i){let u=i.submitter,S=u.pending();S>0&&i.config.logger(`configure: draining ${S} queued items from the previous configuration`),u.reset().catch(()=>{})}n.hydrateFromStorage(),o.start();let p=n.refresh().catch(u=>{e.logger("assignments refresh failed",u);try{e.errorCallback?.(u)}catch{}});i={config:e,user:t,cache:n,dedup:new d,submitter:o,refreshPromise:p}}function O(){return i?.refreshPromise??Promise.resolve()}function $(r){return m("resolveParameter",void 0,l(),()=>{let e=_(),t=e.cache.resolveAssignment(r);if(!t){e.config.logger(`resolveParameter: unknown parameter '${r}'`);return}let s=N(e,r);return s&&!e.dedup.seenRecently(P(s))&&e.submitter.queueExposure({...s}),t.value})}function L(r){return m("getExposure",null,l(),()=>N(_(),r))}function D(r,e){m("trackEvent",void 0,l(),()=>{let t=_();t.submitter.queueEvent({event_slug:r,user_id:t.user.userId,occurred_at:new Date().toISOString(),custom_fields:e??{}})})}function B(){return E("flush",void 0,l(),async()=>{await i?.submitter.flush()})}function H(r={}){return E("reset",void 0,l(),async()=>{let e=i;i=null,await e?.submitter.reset(r)})}function N(r,e){let t=r.cache.resolveAssignment(e);return t?.exposure?{parameter_id:t.parameter_id,space_id:t.space_id,resolved_value:t.value,user_id:r.user.userId,exposable_type:t.exposure.exposable_type,exposable_id:t.exposure.exposable_id,audience_id:t.exposure.audience_id,resolved_at:new Date().toISOString()}:null}function _(){if(!i)throw new Error("abmeter is not configured \u2014 call abmeter.configure(...) first");return i}function l(){return i?{logger:i.config.logger,errorCallback:i.config.errorCallback}:{}}var F="0.2.2";return Y(pe);})();