@splitlab/js-client 0.4.0 → 0.6.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/dist/index.cjs CHANGED
@@ -1 +1 @@
1
- "use strict";var h=Object.defineProperty;var U=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var x=(n,e)=>{for(var t in e)h(n,t,{get:e[t],enumerable:!0})},$=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of P(e))!k.call(n,s)&&s!==t&&h(n,s,{get:()=>e[s],enumerable:!(i=U(e,s))||i.enumerable});return n};var L=n=>$(h({},"__esModule",{value:!0}),n);var j={};x(j,{SplitLabClient:()=>v,hashToFloat:()=>u.hashToFloat,murmurhash3:()=>u.murmurhash3});module.exports=L(j);var u=require("@splitlab/core"),p=require("@splitlab/core");var C=require("@splitlab/core");var o=typeof globalThis<"u"&&typeof globalThis.document<"u";function I(){if(!o)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,s=n.screen,r=(0,C.parseUserAgent)(e?.userAgent);return{pathname:t?.pathname??null,hostname:t?.hostname??null,referrer:i?.referrer||null,search_params:t?.search||null,page_title:i?.title||null,hash:t?.hash||null,user_agent:e?.userAgent??null,browser:r.browser,browser_version:r.browser_version,os:r.os,os_version:r.os_version,device_type:r.device_type,screen_width:s?.width??null,screen_height:s?.height??null,viewport_width:n.innerWidth??null,viewport_height:n.innerHeight??null,language:e?.language??null,timezone:O()}}function O(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var D=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],m="__ot_utm";function w(){if(!o)return{};try{let i=globalThis.sessionStorage?.getItem(m);if(i)return JSON.parse(i)}catch{}let n=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of D){let s=n.get(i);s&&(e[i]=s,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(m,JSON.stringify(e))}catch{}return e}var b="_sl_did",f="_sl_sid",z=63072e3,y="__ot_did";function _(n){if(!o)return null;try{let e=globalThis.document?.cookie?.match(new RegExp("(?:^|; )"+n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}catch{return null}}function g(n,e,t){if(o)try{globalThis.document.cookie=`${n}=${encodeURIComponent(e)}; path=/; max-age=${t}; SameSite=Lax`}catch{}}function E(n){if(!o)return c();let e=Math.floor(n/1e3),t=_(f);if(t)return g(f,t,e),t;let i=c();return g(f,i,e),i}function R(){if(!o)return c();let n=_(b);if(n)return n;let e=null;try{let t=globalThis.localStorage;e=t?.getItem(y)||null,e&&t?.removeItem(y)}catch{}return e||(e=c()),g(b,e,z),e}var S="__ot_ref";function T(){if(!o)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(S);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(S,t),t||null}catch{return globalThis.document?.referrer||null}}function c(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var d=typeof globalThis<"u"&&typeof globalThis.document<"u",v=class{constructor(e){this.evalResult=null;this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.initialized=!1;this.beforeUnloadHandler=null;this.lastEtag=null;this.eventSource=null;this.deviceId=null;this.utmParams={};this.initialReferrer=null;this.pageviewCleanup=null;this.trackedExposures=new Set;this.config={apiKey:e.apiKey,baseUrl:e.baseUrl.replace(/\/$/,""),ingestUrl:(e.ingestUrl||e.baseUrl).replace(/\/$/,""),distinctId:e.distinctId??null,attributes:e.attributes||{},autoFlushInterval:e.autoFlushInterval??3e4,autoFlushSize:e.autoFlushSize??20,enableLocalEvaluation:e.enableLocalEvaluation??!0,configRefreshInterval:e.configRefreshInterval??3e4,realtimeUpdates:e.realtimeUpdates??!1,onConfigUpdate:e.onConfigUpdate??null,captureContext:e.captureContext??d,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{},environment:e.environment??"production"},this.config.captureUtm&&(this.utmParams=w()),this.config.captureContext&&(this.initialReferrer=T()),this.deviceId=R()}get effectiveDistinctId(){return this.config.distinctId||this.deviceId}get bucketingId(){return this.deviceId}async initialize(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=t,this.evalResult=(0,p.localEvaluate)(this.serverConfig,this.bucketingId,this.config.attributes)}catch(e){this.serverConfig||(console.warn("SplitLab: config fetch failed, using safe defaults",e),this.serverConfig={experiments:[],flags:[]},this.evalResult={experiments:{},flags:{}})}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.autoFlushInterval),this.config.enableLocalEvaluation&&(this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.config.configRefreshInterval)),this.config.realtimeUpdates&&this.config.enableLocalEvaluation&&this.connectConfigStream(),d&&(this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&d&&this.setupPageviewTracking(),this.initialized=!0}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");let t=this.evalResult.experiments[e]??null;return t!==null&&!this.trackedExposures.has(e)&&(this.trackedExposures.add(e),this.track("$exposure",{experiment_key:e,variant_key:t}).catch(()=>{})),t}isFeatureEnabled(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.flags[e]??!1}async track(e,t){let i=this.enrichProperties(t);this.eventQueue.push({distinct_id:this.effectiveDistinctId,event_name:e,properties:i,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.config.autoFlushSize&&await this.flush()}async trackPageview(e){await this.track("$pageview",e)}setSuperProperties(e){Object.assign(this.config.superProperties,e)}unsetSuperProperty(e){delete this.config.superProperties[e]}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{await this.request("POST","/ingest/batch",{events:e},!0)}catch{this.eventQueue=e.concat(this.eventQueue)}}async identify(e,t){let i=this.effectiveDistinctId;this.config.distinctId=e,i!==e&&await this.track("$identify",{...t,previous_distinct_id:i}),this.config.enableLocalEvaluation||(this.evalResult=null,this.initialized=!1,await this.initialize())}async refresh(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t,notModified:i}=await this.fetchConfig();if(i)return;this.serverConfig=e,this.lastEtag=t,this.evalResult=(0,p.localEvaluate)(this.serverConfig,this.bucketingId,this.config.attributes),this.config.onConfigUpdate&&this.config.onConfigUpdate()}catch{}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}}getDistinctId(){return this.effectiveDistinctId}getDeviceId(){return this.deviceId}setAttributes(e){this.config.attributes={...this.config.attributes,...e}}async destroy(){await this.flush(),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.beforeUnloadHandler&&typeof globalThis.removeEventListener=="function"&&(globalThis.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageviewCleanup&&(this.pageviewCleanup(),this.pageviewCleanup=null),this.initialized=!1}enrichProperties(e){let t={};if(this.config.captureContext){let i=I();if(i){for(let[s,r]of Object.entries(i))r!==null&&r!==""&&r!==void 0&&(t[s]=r);this.initialReferrer&&(t.referrer=this.initialReferrer)}}if(this.config.captureUtm)for(let[i,s]of Object.entries(this.utmParams))s&&(t[i]=s);this.config.trackSessions&&(t.session_id=E(this.config.sessionTimeout)),this.config.persistDeviceId&&this.deviceId&&(t.device_id=this.deviceId);for(let[i,s]of Object.entries(this.config.superProperties))s!==void 0&&(t[i]=s);if(e)for(let[i,s]of Object.entries(e))s!==void 0&&(t[i]=s);return t}setupPageviewTracking(){let e=globalThis,t=e.history;if(!t?.pushState)return;let i=e.location?.href,s=t.pushState.bind(t);t.pushState=(...l)=>{s(...l),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...l)=>{r(...l),this.onUrlChange(i),i=e.location?.href};let a=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",a),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=s,t.replaceState=r,e.removeEventListener("popstate",a)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}async fetchConfig(){let e={};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",i=await fetch(`${this.config.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.config.apiKey)}${t}`,{method:"GET",headers:e});if(i.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!i.ok){let a=await i.text().catch(()=>"");throw new Error(`SplitLab API error ${i.status}: ${a}`)}let s=i.headers.get("etag");return{config:await i.json(),etag:s}}connectConfigStream(){if(typeof EventSource>"u")return;let e=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",t=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}${e}`;this.eventSource=new EventSource(t),this.eventSource.addEventListener("config_updated",()=>{this.refresh().catch(()=>{})}),this.eventSource.onerror=()=>{}}flushSync(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];let t=JSON.stringify({events:e}),i=`${this.config.ingestUrl}/ingest/batch`;if(typeof globalThis.navigator?.sendBeacon=="function"){let s=new Blob([t],{type:"application/json"});try{if(globalThis.navigator.sendBeacon(i,s))return}catch{}}if(typeof fetch=="function")try{fetch(i,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:t,keepalive:!0}).catch(()=>{})}catch{}}async request(e,t,i,s){let r=s?this.config.ingestUrl:this.config.baseUrl,a=await fetch(`${r}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!a.ok){let l=await a.text().catch(()=>"");throw new Error(`SplitLab API error ${a.status}: ${l}`)}return a.json()}};0&&(module.exports={SplitLabClient,hashToFloat,murmurhash3});
1
+ "use strict";var f=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var x=Object.getOwnPropertyNames;var $=Object.prototype.hasOwnProperty;var D=(n,e)=>{for(var t in e)f(n,t,{get:e[t],enumerable:!0})},L=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of x(e))!$.call(n,s)&&s!==t&&f(n,s,{get:()=>e[s],enumerable:!(i=P(e,s))||i.enumerable});return n};var O=n=>L(f({},"__esModule",{value:!0}),n);var B={};D(B,{SplitLabClient:()=>b,hashToFloat:()=>g.hashToFloat,murmurhash3:()=>g.murmurhash3});module.exports=O(B);var g=require("@splitlab/core"),m=require("@splitlab/core");var w=require("@splitlab/core");var o=typeof globalThis<"u"&&typeof globalThis.document<"u";function I(){if(!o)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,s=n.screen,r=(0,w.parseUserAgent)(e?.userAgent);return{pathname:t?.pathname??null,hostname:t?.hostname??null,referrer:i?.referrer||null,search_params:t?.search||null,page_title:i?.title||null,hash:t?.hash||null,user_agent:e?.userAgent??null,browser:r.browser,browser_version:r.browser_version,os:r.os,os_version:r.os_version,device_type:r.device_type,screen_width:s?.width??null,screen_height:s?.height??null,viewport_width:n.innerWidth??null,viewport_height:n.innerHeight??null,language:e?.language??null,timezone:z()}}function z(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],y="__ot_utm";function _(){if(!o)return{};try{let i=globalThis.sessionStorage?.getItem(y);if(i)return JSON.parse(i)}catch{}let n=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of j){let s=n.get(i);s&&(e[i]=s,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(y,JSON.stringify(e))}catch{}return e}var c="_sl_did",d="_sl_sid",E=63072e3,C="__ot_did";function v(n){if(!o)return null;try{let e=globalThis.document?.cookie?.match(new RegExp("(?:^|; )"+n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}catch{return null}}function u(n,e,t){if(o)try{globalThis.document.cookie=`${n}=${encodeURIComponent(e)}; path=/; max-age=${t}; SameSite=Lax`}catch{}}function T(n){if(!o)return h();let e=Math.floor(n/1e3),t=v(d);if(t)return u(d,t,e),t;let i=h();return u(d,i,e),i}function R(){if(!o)return h();let n=v(c);if(n)return n;let e=null;try{let t=globalThis.localStorage;e=t?.getItem(C)||null,e&&t?.removeItem(C)}catch{}return e||(e=h()),u(c,e,E),e}function U(n){!o||v(c)===n||u(c,n,E)}var S="__ot_ref";function k(){if(!o)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(S);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(S,t),t||null}catch{return globalThis.document?.referrer||null}}function h(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var p=typeof globalThis<"u"&&typeof globalThis.document<"u",b=class{constructor(e){this.evalResult=null;this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.initialized=!1;this.visibilityHandler=null;this.beforeUnloadHandler=null;this.lastEtag=null;this.eventSource=null;this.deviceId=null;this.utmParams={};this.initialReferrer=null;this.pageviewCleanup=null;this.trackedExposures=new Set;this.config={apiKey:e.apiKey,baseUrl:e.baseUrl.replace(/\/$/,""),ingestUrl:(e.ingestUrl||e.baseUrl).replace(/\/$/,""),distinctId:e.distinctId??null,attributes:e.attributes||{},autoFlushInterval:e.autoFlushInterval??8e3,autoFlushSize:e.autoFlushSize??20,enableLocalEvaluation:e.enableLocalEvaluation??!0,realtimeUpdates:e.realtimeUpdates??!1,onConfigUpdate:e.onConfigUpdate??null,captureContext:e.captureContext??p,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{},environment:e.environment??"production"},this.config.captureUtm&&(this.utmParams=_()),this.config.captureContext&&(this.initialReferrer=k()),e.bootstrap?.deviceId?(this.deviceId=e.bootstrap.deviceId,U(e.bootstrap.deviceId)):this.deviceId=R()}get effectiveDistinctId(){return this.config.distinctId||this.deviceId}get bucketingId(){return this.deviceId}async initialize(){let e=this.initialized;if(!e)if(this.config.enableLocalEvaluation)try{let{config:t,etag:i}=await this.fetchConfig();this.serverConfig=t,this.lastEtag=i,this.evalResult=(0,m.localEvaluate)(this.serverConfig,this.bucketingId,this.config.attributes)}catch(t){this.serverConfig||(console.warn("SplitLab: config fetch failed, using safe defaults",t),this.serverConfig={experiments:[],flags:[]},this.evalResult={experiments:{},flags:{}})}else{let t={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(t.attributes=this.config.attributes);let i=await this.request("POST","/api/sdk/evaluate",t);this.evalResult={experiments:i.experiments,flags:i.flags}}this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.autoFlushInterval),this.config.realtimeUpdates&&this.config.enableLocalEvaluation&&this.connectConfigStream(),p&&(this.visibilityHandler=()=>{globalThis.document?.visibilityState==="hidden"?this.flushSync():this.config.enableLocalEvaluation&&this.refresh().catch(()=>{})},globalThis.document?.addEventListener("visibilitychange",this.visibilityHandler),this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&p&&this.setupPageviewTracking(),this.initialized=!0,e&&this.refresh().catch(()=>{})}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");let t=this.evalResult.experiments[e]??null;return t!==null&&!this.trackedExposures.has(e)&&(this.trackedExposures.add(e),this.track("$exposure",{experiment_key:e,variant_key:t}).catch(()=>{})),t}isFeatureEnabled(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.flags[e]??!1}hydrateFromBootstrap(e){this.serverConfig=e.serverConfig,this.lastEtag=e.etag,this.evalResult=e.evalResult,this.initialized=!0}async track(e,t){let i=this.enrichProperties(t);this.eventQueue.push({distinct_id:this.effectiveDistinctId,event_name:e,properties:i,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.config.autoFlushSize&&await this.flush()}async trackPageview(e){await this.track("$pageview",e)}setSuperProperties(e){Object.assign(this.config.superProperties,e)}unsetSuperProperty(e){delete this.config.superProperties[e]}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{await this.request("POST","/ingest/batch",{events:e},!0)}catch{this.eventQueue=e.concat(this.eventQueue)}}async identify(e,t){let i=this.effectiveDistinctId;this.config.distinctId=e,i!==e&&await this.track("$identify",{...t,previous_distinct_id:i}),this.config.enableLocalEvaluation||(this.evalResult=null,this.initialized=!1,await this.initialize())}async refresh(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t,notModified:i}=await this.fetchConfig();if(i)return;this.serverConfig=e,this.lastEtag=t,this.evalResult=(0,m.localEvaluate)(this.serverConfig,this.bucketingId,this.config.attributes),this.config.onConfigUpdate&&this.config.onConfigUpdate()}catch{}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}}getDistinctId(){return this.effectiveDistinctId}getDeviceId(){return this.deviceId}setAttributes(e){this.config.attributes={...this.config.attributes,...e}}async destroy(){await this.flush(),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.visibilityHandler&&(globalThis.document?.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.beforeUnloadHandler&&typeof globalThis.removeEventListener=="function"&&(globalThis.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageviewCleanup&&(this.pageviewCleanup(),this.pageviewCleanup=null),this.initialized=!1}enrichProperties(e){let t={};if(this.config.captureContext){let i=I();if(i){for(let[s,r]of Object.entries(i))r!==null&&r!==""&&r!==void 0&&(t[s]=r);this.initialReferrer&&(t.referrer=this.initialReferrer)}}if(this.config.captureUtm)for(let[i,s]of Object.entries(this.utmParams))s&&(t[i]=s);this.config.trackSessions&&(t.session_id=T(this.config.sessionTimeout)),this.config.persistDeviceId&&this.deviceId&&(t.device_id=this.deviceId);for(let[i,s]of Object.entries(this.config.superProperties))s!==void 0&&(t[i]=s);if(e)for(let[i,s]of Object.entries(e))s!==void 0&&(t[i]=s);return t}setupPageviewTracking(){let e=globalThis,t=e.history;if(!t?.pushState)return;let i=e.location?.href,s=t.pushState.bind(t);t.pushState=(...l)=>{s(...l),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...l)=>{r(...l),this.onUrlChange(i),i=e.location?.href};let a=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",a),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=s,t.replaceState=r,e.removeEventListener("popstate",a)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}async fetchConfig(){let e={};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",i=await fetch(`${this.config.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.config.apiKey)}${t}`,{method:"GET",headers:e});if(i.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!i.ok){let a=await i.text().catch(()=>"");throw new Error(`SplitLab API error ${i.status}: ${a}`)}let s=i.headers.get("etag");return{config:await i.json(),etag:s}}connectConfigStream(){if(typeof EventSource>"u")return;let e=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",t=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}${e}`;this.eventSource=new EventSource(t),this.eventSource.addEventListener("config_updated",()=>{this.refresh().catch(()=>{})}),this.eventSource.onerror=()=>{}}flushSync(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];let t=JSON.stringify({events:e}),i=`${this.config.ingestUrl}/ingest/batch?key=${encodeURIComponent(this.config.apiKey)}`;if(typeof globalThis.navigator?.sendBeacon=="function"){let s=new Blob([t],{type:"application/json"});try{if(globalThis.navigator.sendBeacon(i,s))return}catch{}}if(typeof fetch=="function")try{fetch(i,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:t,keepalive:!0}).catch(()=>{})}catch{}}async request(e,t,i,s){let r=s?this.config.ingestUrl:this.config.baseUrl,a=await fetch(`${r}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!a.ok){let l=await a.text().catch(()=>"");throw new Error(`SplitLab API error ${a.status}: ${l}`)}return a.json()}};0&&(module.exports={SplitLabClient,hashToFloat,murmurhash3});
package/dist/index.d.cts CHANGED
@@ -1,4 +1,6 @@
1
- export { EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
1
+ import * as _splitlab_core from '@splitlab/core';
2
+ import { BootstrapData } from '@splitlab/core';
3
+ export { BootstrapData, EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
2
4
 
3
5
  interface SplitLabConfig {
4
6
  apiKey: string;
@@ -7,12 +9,11 @@ interface SplitLabConfig {
7
9
  /** Unique user identifier. If omitted, the SDK auto-generates a persistent device ID. */
8
10
  distinctId?: string;
9
11
  attributes?: Record<string, any>;
12
+ /** Event flush interval in ms. Default: 8000 (8s). */
10
13
  autoFlushInterval?: number;
11
14
  autoFlushSize?: number;
12
15
  /** Enable local evaluation of experiments/flags (no /evaluate network call). Default: true. */
13
16
  enableLocalEvaluation?: boolean;
14
- /** Config polling interval in ms. Default: 30000 (30s). */
15
- configRefreshInterval?: number;
16
17
  /** Enable SSE for instant config push from server. Default: false. */
17
18
  realtimeUpdates?: boolean;
18
19
  /** Callback when config changes (after refresh). */
@@ -33,6 +34,8 @@ interface SplitLabConfig {
33
34
  superProperties?: Record<string, any>;
34
35
  /** Environment to fetch config for (e.g. 'development', 'staging', 'production'). Default: 'production'. */
35
36
  environment?: string;
37
+ /** Bootstrap data from server-side rendering for instant hydration (no config fetch). */
38
+ bootstrap?: _splitlab_core.BootstrapData;
36
39
  }
37
40
 
38
41
  interface BrowserContext {
@@ -64,8 +67,8 @@ declare class SplitLabClient {
64
67
  private serverConfig;
65
68
  private eventQueue;
66
69
  private flushTimer;
67
- private configRefreshTimer;
68
70
  private initialized;
71
+ private visibilityHandler;
69
72
  private beforeUnloadHandler;
70
73
  private lastEtag;
71
74
  private eventSource;
@@ -82,6 +85,12 @@ declare class SplitLabClient {
82
85
  initialize(): Promise<void>;
83
86
  getVariant(experimentKey: string): string | null;
84
87
  isFeatureEnabled(flagKey: string): boolean;
88
+ /**
89
+ * Hydrate the client from server-generated bootstrap data.
90
+ * Synchronous, pure state assignment — safe to call during React render.
91
+ * Exposures are NOT fired here; they fire on first `getVariant()` access.
92
+ */
93
+ hydrateFromBootstrap(bootstrap: BootstrapData): void;
85
94
  track(eventName: string, properties?: Record<string, any>): Promise<void>;
86
95
  /** Track a pageview event with the current URL context. */
87
96
  trackPageview(properties?: Record<string, any>): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
- export { EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
1
+ import * as _splitlab_core from '@splitlab/core';
2
+ import { BootstrapData } from '@splitlab/core';
3
+ export { BootstrapData, EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
2
4
 
3
5
  interface SplitLabConfig {
4
6
  apiKey: string;
@@ -7,12 +9,11 @@ interface SplitLabConfig {
7
9
  /** Unique user identifier. If omitted, the SDK auto-generates a persistent device ID. */
8
10
  distinctId?: string;
9
11
  attributes?: Record<string, any>;
12
+ /** Event flush interval in ms. Default: 8000 (8s). */
10
13
  autoFlushInterval?: number;
11
14
  autoFlushSize?: number;
12
15
  /** Enable local evaluation of experiments/flags (no /evaluate network call). Default: true. */
13
16
  enableLocalEvaluation?: boolean;
14
- /** Config polling interval in ms. Default: 30000 (30s). */
15
- configRefreshInterval?: number;
16
17
  /** Enable SSE for instant config push from server. Default: false. */
17
18
  realtimeUpdates?: boolean;
18
19
  /** Callback when config changes (after refresh). */
@@ -33,6 +34,8 @@ interface SplitLabConfig {
33
34
  superProperties?: Record<string, any>;
34
35
  /** Environment to fetch config for (e.g. 'development', 'staging', 'production'). Default: 'production'. */
35
36
  environment?: string;
37
+ /** Bootstrap data from server-side rendering for instant hydration (no config fetch). */
38
+ bootstrap?: _splitlab_core.BootstrapData;
36
39
  }
37
40
 
38
41
  interface BrowserContext {
@@ -64,8 +67,8 @@ declare class SplitLabClient {
64
67
  private serverConfig;
65
68
  private eventQueue;
66
69
  private flushTimer;
67
- private configRefreshTimer;
68
70
  private initialized;
71
+ private visibilityHandler;
69
72
  private beforeUnloadHandler;
70
73
  private lastEtag;
71
74
  private eventSource;
@@ -82,6 +85,12 @@ declare class SplitLabClient {
82
85
  initialize(): Promise<void>;
83
86
  getVariant(experimentKey: string): string | null;
84
87
  isFeatureEnabled(flagKey: string): boolean;
88
+ /**
89
+ * Hydrate the client from server-generated bootstrap data.
90
+ * Synchronous, pure state assignment — safe to call during React render.
91
+ * Exposures are NOT fired here; they fire on first `getVariant()` access.
92
+ */
93
+ hydrateFromBootstrap(bootstrap: BootstrapData): void;
85
94
  track(eventName: string, properties?: Record<string, any>): Promise<void>;
86
95
  /** Track a pageview event with the current URL context. */
87
96
  trackPageview(properties?: Record<string, any>): Promise<void>;
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{murmurhash3 as D,hashToFloat as z}from"@splitlab/core";import{localEvaluate as w}from"@splitlab/core";import{parseUserAgent as E}from"@splitlab/core";var o=typeof globalThis<"u"&&typeof globalThis.document<"u";function m(){if(!o)return null;let s=globalThis,e=s.navigator,t=s.location,i=s.document,n=s.screen,r=E(e?.userAgent);return{pathname:t?.pathname??null,hostname:t?.hostname??null,referrer:i?.referrer||null,search_params:t?.search||null,page_title:i?.title||null,hash:t?.hash||null,user_agent:e?.userAgent??null,browser:r.browser,browser_version:r.browser_version,os:r.os,os_version:r.os_version,device_type:r.device_type,screen_width:n?.width??null,screen_height:n?.height??null,viewport_width:s.innerWidth??null,viewport_height:s.innerHeight??null,language:e?.language??null,timezone:R()}}function R(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var T=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],g="__ot_utm";function b(){if(!o)return{};try{let i=globalThis.sessionStorage?.getItem(g);if(i)return JSON.parse(i)}catch{}let s=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of T){let n=s.get(i);n&&(e[i]=n,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(g,JSON.stringify(e))}catch{}return e}var d="_sl_did",u="_sl_sid",U=63072e3,p="__ot_did";function y(s){if(!o)return null;try{let e=globalThis.document?.cookie?.match(new RegExp("(?:^|; )"+s.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}catch{return null}}function h(s,e,t){if(o)try{globalThis.document.cookie=`${s}=${encodeURIComponent(e)}; path=/; max-age=${t}; SameSite=Lax`}catch{}}function S(s){if(!o)return c();let e=Math.floor(s/1e3),t=y(u);if(t)return h(u,t,e),t;let i=c();return h(u,i,e),i}function C(){if(!o)return c();let s=y(d);if(s)return s;let e=null;try{let t=globalThis.localStorage;e=t?.getItem(p)||null,e&&t?.removeItem(p)}catch{}return e||(e=c()),h(d,e,U),e}var v="__ot_ref";function I(){if(!o)return null;try{let s=globalThis.sessionStorage,e=s?.getItem(v);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return s?.setItem(v,t),t||null}catch{return globalThis.document?.referrer||null}}function c(){let s=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${s}-${e}`}var f=typeof globalThis<"u"&&typeof globalThis.document<"u",_=class{constructor(e){this.evalResult=null;this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.initialized=!1;this.beforeUnloadHandler=null;this.lastEtag=null;this.eventSource=null;this.deviceId=null;this.utmParams={};this.initialReferrer=null;this.pageviewCleanup=null;this.trackedExposures=new Set;this.config={apiKey:e.apiKey,baseUrl:e.baseUrl.replace(/\/$/,""),ingestUrl:(e.ingestUrl||e.baseUrl).replace(/\/$/,""),distinctId:e.distinctId??null,attributes:e.attributes||{},autoFlushInterval:e.autoFlushInterval??3e4,autoFlushSize:e.autoFlushSize??20,enableLocalEvaluation:e.enableLocalEvaluation??!0,configRefreshInterval:e.configRefreshInterval??3e4,realtimeUpdates:e.realtimeUpdates??!1,onConfigUpdate:e.onConfigUpdate??null,captureContext:e.captureContext??f,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{},environment:e.environment??"production"},this.config.captureUtm&&(this.utmParams=b()),this.config.captureContext&&(this.initialReferrer=I()),this.deviceId=C()}get effectiveDistinctId(){return this.config.distinctId||this.deviceId}get bucketingId(){return this.deviceId}async initialize(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=t,this.evalResult=w(this.serverConfig,this.bucketingId,this.config.attributes)}catch(e){this.serverConfig||(console.warn("SplitLab: config fetch failed, using safe defaults",e),this.serverConfig={experiments:[],flags:[]},this.evalResult={experiments:{},flags:{}})}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.autoFlushInterval),this.config.enableLocalEvaluation&&(this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.config.configRefreshInterval)),this.config.realtimeUpdates&&this.config.enableLocalEvaluation&&this.connectConfigStream(),f&&(this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&f&&this.setupPageviewTracking(),this.initialized=!0}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");let t=this.evalResult.experiments[e]??null;return t!==null&&!this.trackedExposures.has(e)&&(this.trackedExposures.add(e),this.track("$exposure",{experiment_key:e,variant_key:t}).catch(()=>{})),t}isFeatureEnabled(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.flags[e]??!1}async track(e,t){let i=this.enrichProperties(t);this.eventQueue.push({distinct_id:this.effectiveDistinctId,event_name:e,properties:i,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.config.autoFlushSize&&await this.flush()}async trackPageview(e){await this.track("$pageview",e)}setSuperProperties(e){Object.assign(this.config.superProperties,e)}unsetSuperProperty(e){delete this.config.superProperties[e]}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{await this.request("POST","/ingest/batch",{events:e},!0)}catch{this.eventQueue=e.concat(this.eventQueue)}}async identify(e,t){let i=this.effectiveDistinctId;this.config.distinctId=e,i!==e&&await this.track("$identify",{...t,previous_distinct_id:i}),this.config.enableLocalEvaluation||(this.evalResult=null,this.initialized=!1,await this.initialize())}async refresh(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t,notModified:i}=await this.fetchConfig();if(i)return;this.serverConfig=e,this.lastEtag=t,this.evalResult=w(this.serverConfig,this.bucketingId,this.config.attributes),this.config.onConfigUpdate&&this.config.onConfigUpdate()}catch{}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}}getDistinctId(){return this.effectiveDistinctId}getDeviceId(){return this.deviceId}setAttributes(e){this.config.attributes={...this.config.attributes,...e}}async destroy(){await this.flush(),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.beforeUnloadHandler&&typeof globalThis.removeEventListener=="function"&&(globalThis.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageviewCleanup&&(this.pageviewCleanup(),this.pageviewCleanup=null),this.initialized=!1}enrichProperties(e){let t={};if(this.config.captureContext){let i=m();if(i){for(let[n,r]of Object.entries(i))r!==null&&r!==""&&r!==void 0&&(t[n]=r);this.initialReferrer&&(t.referrer=this.initialReferrer)}}if(this.config.captureUtm)for(let[i,n]of Object.entries(this.utmParams))n&&(t[i]=n);this.config.trackSessions&&(t.session_id=S(this.config.sessionTimeout)),this.config.persistDeviceId&&this.deviceId&&(t.device_id=this.deviceId);for(let[i,n]of Object.entries(this.config.superProperties))n!==void 0&&(t[i]=n);if(e)for(let[i,n]of Object.entries(e))n!==void 0&&(t[i]=n);return t}setupPageviewTracking(){let e=globalThis,t=e.history;if(!t?.pushState)return;let i=e.location?.href,n=t.pushState.bind(t);t.pushState=(...l)=>{n(...l),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...l)=>{r(...l),this.onUrlChange(i),i=e.location?.href};let a=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",a),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=n,t.replaceState=r,e.removeEventListener("popstate",a)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}async fetchConfig(){let e={};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",i=await fetch(`${this.config.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.config.apiKey)}${t}`,{method:"GET",headers:e});if(i.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!i.ok){let a=await i.text().catch(()=>"");throw new Error(`SplitLab API error ${i.status}: ${a}`)}let n=i.headers.get("etag");return{config:await i.json(),etag:n}}connectConfigStream(){if(typeof EventSource>"u")return;let e=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",t=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}${e}`;this.eventSource=new EventSource(t),this.eventSource.addEventListener("config_updated",()=>{this.refresh().catch(()=>{})}),this.eventSource.onerror=()=>{}}flushSync(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];let t=JSON.stringify({events:e}),i=`${this.config.ingestUrl}/ingest/batch`;if(typeof globalThis.navigator?.sendBeacon=="function"){let n=new Blob([t],{type:"application/json"});try{if(globalThis.navigator.sendBeacon(i,n))return}catch{}}if(typeof fetch=="function")try{fetch(i,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:t,keepalive:!0}).catch(()=>{})}catch{}}async request(e,t,i,n){let r=n?this.config.ingestUrl:this.config.baseUrl,a=await fetch(`${r}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!a.ok){let l=await a.text().catch(()=>"");throw new Error(`SplitLab API error ${a.status}: ${l}`)}return a.json()}};export{_ as SplitLabClient,z as hashToFloat,D as murmurhash3};
1
+ import{murmurhash3 as z,hashToFloat as j}from"@splitlab/core";import{localEvaluate as E}from"@splitlab/core";import{parseUserAgent as R}from"@splitlab/core";var o=typeof globalThis<"u"&&typeof globalThis.document<"u";function b(){if(!o)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,s=n.screen,r=R(e?.userAgent);return{pathname:t?.pathname??null,hostname:t?.hostname??null,referrer:i?.referrer||null,search_params:t?.search||null,page_title:i?.title||null,hash:t?.hash||null,user_agent:e?.userAgent??null,browser:r.browser,browser_version:r.browser_version,os:r.os,os_version:r.os_version,device_type:r.device_type,screen_width:s?.width??null,screen_height:s?.height??null,viewport_width:n.innerWidth??null,viewport_height:n.innerHeight??null,language:e?.language??null,timezone:U()}}function U(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var k=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],v="__ot_utm";function y(){if(!o)return{};try{let i=globalThis.sessionStorage?.getItem(v);if(i)return JSON.parse(i)}catch{}let n=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of k){let s=n.get(i);s&&(e[i]=s,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(v,JSON.stringify(e))}catch{}return e}var c="_sl_did",g="_sl_sid",C=63072e3,p="__ot_did";function f(n){if(!o)return null;try{let e=globalThis.document?.cookie?.match(new RegExp("(?:^|; )"+n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}catch{return null}}function u(n,e,t){if(o)try{globalThis.document.cookie=`${n}=${encodeURIComponent(e)}; path=/; max-age=${t}; SameSite=Lax`}catch{}}function S(n){if(!o)return h();let e=Math.floor(n/1e3),t=f(g);if(t)return u(g,t,e),t;let i=h();return u(g,i,e),i}function w(){if(!o)return h();let n=f(c);if(n)return n;let e=null;try{let t=globalThis.localStorage;e=t?.getItem(p)||null,e&&t?.removeItem(p)}catch{}return e||(e=h()),u(c,e,C),e}function I(n){!o||f(c)===n||u(c,n,C)}var m="__ot_ref";function _(){if(!o)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(m);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(m,t),t||null}catch{return globalThis.document?.referrer||null}}function h(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var d=typeof globalThis<"u"&&typeof globalThis.document<"u",T=class{constructor(e){this.evalResult=null;this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.initialized=!1;this.visibilityHandler=null;this.beforeUnloadHandler=null;this.lastEtag=null;this.eventSource=null;this.deviceId=null;this.utmParams={};this.initialReferrer=null;this.pageviewCleanup=null;this.trackedExposures=new Set;this.config={apiKey:e.apiKey,baseUrl:e.baseUrl.replace(/\/$/,""),ingestUrl:(e.ingestUrl||e.baseUrl).replace(/\/$/,""),distinctId:e.distinctId??null,attributes:e.attributes||{},autoFlushInterval:e.autoFlushInterval??8e3,autoFlushSize:e.autoFlushSize??20,enableLocalEvaluation:e.enableLocalEvaluation??!0,realtimeUpdates:e.realtimeUpdates??!1,onConfigUpdate:e.onConfigUpdate??null,captureContext:e.captureContext??d,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{},environment:e.environment??"production"},this.config.captureUtm&&(this.utmParams=y()),this.config.captureContext&&(this.initialReferrer=_()),e.bootstrap?.deviceId?(this.deviceId=e.bootstrap.deviceId,I(e.bootstrap.deviceId)):this.deviceId=w()}get effectiveDistinctId(){return this.config.distinctId||this.deviceId}get bucketingId(){return this.deviceId}async initialize(){let e=this.initialized;if(!e)if(this.config.enableLocalEvaluation)try{let{config:t,etag:i}=await this.fetchConfig();this.serverConfig=t,this.lastEtag=i,this.evalResult=E(this.serverConfig,this.bucketingId,this.config.attributes)}catch(t){this.serverConfig||(console.warn("SplitLab: config fetch failed, using safe defaults",t),this.serverConfig={experiments:[],flags:[]},this.evalResult={experiments:{},flags:{}})}else{let t={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(t.attributes=this.config.attributes);let i=await this.request("POST","/api/sdk/evaluate",t);this.evalResult={experiments:i.experiments,flags:i.flags}}this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.autoFlushInterval),this.config.realtimeUpdates&&this.config.enableLocalEvaluation&&this.connectConfigStream(),d&&(this.visibilityHandler=()=>{globalThis.document?.visibilityState==="hidden"?this.flushSync():this.config.enableLocalEvaluation&&this.refresh().catch(()=>{})},globalThis.document?.addEventListener("visibilitychange",this.visibilityHandler),this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&d&&this.setupPageviewTracking(),this.initialized=!0,e&&this.refresh().catch(()=>{})}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");let t=this.evalResult.experiments[e]??null;return t!==null&&!this.trackedExposures.has(e)&&(this.trackedExposures.add(e),this.track("$exposure",{experiment_key:e,variant_key:t}).catch(()=>{})),t}isFeatureEnabled(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.flags[e]??!1}hydrateFromBootstrap(e){this.serverConfig=e.serverConfig,this.lastEtag=e.etag,this.evalResult=e.evalResult,this.initialized=!0}async track(e,t){let i=this.enrichProperties(t);this.eventQueue.push({distinct_id:this.effectiveDistinctId,event_name:e,properties:i,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.config.autoFlushSize&&await this.flush()}async trackPageview(e){await this.track("$pageview",e)}setSuperProperties(e){Object.assign(this.config.superProperties,e)}unsetSuperProperty(e){delete this.config.superProperties[e]}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{await this.request("POST","/ingest/batch",{events:e},!0)}catch{this.eventQueue=e.concat(this.eventQueue)}}async identify(e,t){let i=this.effectiveDistinctId;this.config.distinctId=e,i!==e&&await this.track("$identify",{...t,previous_distinct_id:i}),this.config.enableLocalEvaluation||(this.evalResult=null,this.initialized=!1,await this.initialize())}async refresh(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t,notModified:i}=await this.fetchConfig();if(i)return;this.serverConfig=e,this.lastEtag=t,this.evalResult=E(this.serverConfig,this.bucketingId,this.config.attributes),this.config.onConfigUpdate&&this.config.onConfigUpdate()}catch{}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}}getDistinctId(){return this.effectiveDistinctId}getDeviceId(){return this.deviceId}setAttributes(e){this.config.attributes={...this.config.attributes,...e}}async destroy(){await this.flush(),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.visibilityHandler&&(globalThis.document?.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.beforeUnloadHandler&&typeof globalThis.removeEventListener=="function"&&(globalThis.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageviewCleanup&&(this.pageviewCleanup(),this.pageviewCleanup=null),this.initialized=!1}enrichProperties(e){let t={};if(this.config.captureContext){let i=b();if(i){for(let[s,r]of Object.entries(i))r!==null&&r!==""&&r!==void 0&&(t[s]=r);this.initialReferrer&&(t.referrer=this.initialReferrer)}}if(this.config.captureUtm)for(let[i,s]of Object.entries(this.utmParams))s&&(t[i]=s);this.config.trackSessions&&(t.session_id=S(this.config.sessionTimeout)),this.config.persistDeviceId&&this.deviceId&&(t.device_id=this.deviceId);for(let[i,s]of Object.entries(this.config.superProperties))s!==void 0&&(t[i]=s);if(e)for(let[i,s]of Object.entries(e))s!==void 0&&(t[i]=s);return t}setupPageviewTracking(){let e=globalThis,t=e.history;if(!t?.pushState)return;let i=e.location?.href,s=t.pushState.bind(t);t.pushState=(...l)=>{s(...l),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...l)=>{r(...l),this.onUrlChange(i),i=e.location?.href};let a=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",a),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=s,t.replaceState=r,e.removeEventListener("popstate",a)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}async fetchConfig(){let e={};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",i=await fetch(`${this.config.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.config.apiKey)}${t}`,{method:"GET",headers:e});if(i.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!i.ok){let a=await i.text().catch(()=>"");throw new Error(`SplitLab API error ${i.status}: ${a}`)}let s=i.headers.get("etag");return{config:await i.json(),etag:s}}connectConfigStream(){if(typeof EventSource>"u")return;let e=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",t=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}${e}`;this.eventSource=new EventSource(t),this.eventSource.addEventListener("config_updated",()=>{this.refresh().catch(()=>{})}),this.eventSource.onerror=()=>{}}flushSync(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];let t=JSON.stringify({events:e}),i=`${this.config.ingestUrl}/ingest/batch?key=${encodeURIComponent(this.config.apiKey)}`;if(typeof globalThis.navigator?.sendBeacon=="function"){let s=new Blob([t],{type:"application/json"});try{if(globalThis.navigator.sendBeacon(i,s))return}catch{}}if(typeof fetch=="function")try{fetch(i,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:t,keepalive:!0}).catch(()=>{})}catch{}}async request(e,t,i,s){let r=s?this.config.ingestUrl:this.config.baseUrl,a=await fetch(`${r}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!a.ok){let l=await a.text().catch(()=>"");throw new Error(`SplitLab API error ${a.status}: ${l}`)}return a.json()}};export{T as SplitLabClient,j as hashToFloat,z as murmurhash3};
@@ -1 +1 @@
1
- "use strict";var SplitLab=(()=>{var m=Object.defineProperty;var F=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var B=Object.prototype.hasOwnProperty;var N=(n,e)=>{for(var t in e)m(n,t,{get:e[t],enumerable:!0})},K=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of j(e))!B.call(n,a)&&a!==t&&m(n,a,{get:()=>e[a],enumerable:!(i=F(e,a))||i.enumerable});return n};var Q=n=>K(m({},"__esModule",{value:!0}),n);var J={};N(J,{SplitLabClient:()=>w,hashToFloat:()=>I,murmurhash3:()=>g});function g(n,e=0){let t=e>>>0,i=n.length,a=i>>2,r=3432918353,o=461845907;for(let l=0;l<a;l++){let c=n.charCodeAt(l*4)&255|(n.charCodeAt(l*4+1)&255)<<8|(n.charCodeAt(l*4+2)&255)<<16|(n.charCodeAt(l*4+3)&255)<<24;c=Math.imul(c,r),c=c<<15|c>>>17,c=Math.imul(c,o),t^=c,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196}let u=a*4,s=0;switch(i&3){case 3:s^=(n.charCodeAt(u+2)&255)<<16;case 2:s^=(n.charCodeAt(u+1)&255)<<8;case 1:s^=n.charCodeAt(u)&255,s=Math.imul(s,r),s=s<<15|s>>>17,s=Math.imul(s,o),t^=s}return t^=i,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function I(n,e){return g(n,e)/4294967295}function b(n,e,t={}){let i={},a={},r=n.segments||[],o=n.exclusion_groups||[],u=new Map;for(let s of o)for(let[l,c]of Object.entries(s.experiments))u.set(l,{layer_key:s.layer_key,...c});for(let s of n.experiments){if(s.targeting_rules&&!p(s.targeting_rules,t,r)){i[s.key]=null;continue}let l=u.get(s.key);if(l){let h=g(l.layer_key+":"+e)%1e4;if(h<l.bucket_start||h>=l.bucket_end){i[s.key]=null;continue}}let c=g(s.key+":"+e);if(c%1e4/100>=s.traffic_percentage){i[s.key]=null;continue}let M=s.variants.reduce((h,z)=>h+z.weight,0),D=c%M,_=0,d=null;for(let h of s.variants)if(_+=h.weight,D<_){d=h.key;break}d||(d=s.variants[s.variants.length-1].key),i[s.key]=d}for(let s of n.flags){if(s.rules&&!p(s.rules,t,r)){a[s.key]=!1;continue}let l=g(s.key+":"+e)%100;a[s.key]=l<s.rollout_percentage}return{experiments:i,flags:a}}function p(n,e,t=[]){if(!n.groups||n.groups.length===0)return!0;let i=r=>{if(r.attribute==="$segment"){if(!Array.isArray(r.value))return!1;let u=r.value.map(String);return r.operator==="in"?u.some(s=>{let l=t.find(c=>c.id===s||c.key===s);return l?p(l.rules,e,t):!1}):r.operator==="not_in"?!u.some(s=>{let l=t.find(c=>c.id===s||c.key===s);return l?p(l.rules,e,t):!1}):!1}let o=e[r.attribute];if(o==null)return!1;switch(r.operator){case"is":return String(o)===String(r.value);case"is_not":return String(o)!==String(r.value);case"contains":return String(o).toLowerCase().includes(String(r.value).toLowerCase());case"not_contains":return!String(o).toLowerCase().includes(String(r.value).toLowerCase());case"gt":return Number(o)>Number(r.value);case"lt":return Number(o)<Number(r.value);case"gte":return Number(o)>=Number(r.value);case"lte":return Number(o)<=Number(r.value);case"in":return Array.isArray(r.value)&&r.value.map(String).includes(String(o));case"not_in":return Array.isArray(r.value)&&!r.value.map(String).includes(String(o));default:return!1}},a=r=>r.conditions.every(i);return n.match==="any"?n.groups.some(a):n.groups.every(a)}var H=[[/Edg(?:e|A|iOS)?\/(\S+)/,"Edge"],[/OPR\/(\S+)/,"Opera"],[/SamsungBrowser\/(\S+)/,"Samsung Internet"],[/UCBrowser\/(\S+)/,"UC Browser"],[/Firefox\/(\S+)/,"Firefox"],[/CriOS\/(\S+)/,"Chrome"],[/FxiOS\/(\S+)/,"Firefox"],[/Chrome\/(\S+)/,"Chrome"],[/Safari\/(\S+)/,"Safari"]],G=[[/Windows NT (\d+\.\d+)/,"Windows"],[/Mac OS X ([\d_]+)/,"macOS"],[/Android ([\d.]+)/,"Android"],[/iPhone OS ([\d_]+)/,"iOS"],[/iPad.*OS ([\d_]+)/,"iPadOS"],[/CrOS/,"ChromeOS"],[/Linux/,"Linux"]];function k(n){if(!n)return{browser:null,browser_version:null,os:null,os_version:null,device_type:null};let e=null,t=null,i=null,a=null;if(/Safari/.test(n)&&!/Chrome|CriOS|Chromium|Edg|OPR|SamsungBrowser|UCBrowser/.test(n)){let o=n.match(/Version\/(\S+)/);e="Safari",t=o?o[1]:null}else for(let[o,u]of H){let s=n.match(o);if(s){e=u,t=s[1]||null;break}}for(let[o,u]of G){let s=n.match(o);if(s){i=u,a=s[1]?.replace(/_/g,".")||null;break}}let r="desktop";return/Mobi|Android.*Mobile|iPhone/.test(n)?r="mobile":/iPad|Android(?!.*Mobile)|Tablet/.test(n)&&(r="tablet"),{browser:e,browser_version:t,os:i,os_version:a,device_type:r}}var f=typeof globalThis<"u"&&typeof globalThis.document<"u";function x(){if(!f)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,a=n.screen,r=k(e?.userAgent);return{pathname:t?.pathname??null,hostname:t?.hostname??null,referrer:i?.referrer||null,search_params:t?.search||null,page_title:i?.title||null,hash:t?.hash||null,user_agent:e?.userAgent??null,browser:r.browser,browser_version:r.browser_version,os:r.os,os_version:r.os_version,device_type:r.device_type,screen_width:a?.width??null,screen_height:a?.height??null,viewport_width:n.innerWidth??null,viewport_height:n.innerHeight??null,language:e?.language??null,timezone:V()}}function V(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var Y=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],E="__ot_utm";function P(){if(!f)return{};try{let i=globalThis.sessionStorage?.getItem(E);if(i)return JSON.parse(i)}catch{}let n=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of Y){let a=n.get(i);a&&(e[i]=a,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(E,JSON.stringify(e))}catch{}return e}var R="_sl_did",y="_sl_sid",q=63072e3,T="__ot_did";function O(n){if(!f)return null;try{let e=globalThis.document?.cookie?.match(new RegExp("(?:^|; )"+n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}catch{return null}}function S(n,e,t){if(f)try{globalThis.document.cookie=`${n}=${encodeURIComponent(e)}; path=/; max-age=${t}; SameSite=Lax`}catch{}}function A(n){if(!f)return v();let e=Math.floor(n/1e3),t=O(y);if(t)return S(y,t,e),t;let i=v();return S(y,i,e),i}function L(){if(!f)return v();let n=O(R);if(n)return n;let e=null;try{let t=globalThis.localStorage;e=t?.getItem(T)||null,e&&t?.removeItem(T)}catch{}return e||(e=v()),S(R,e,q),e}var U="__ot_ref";function $(){if(!f)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(U);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(U,t),t||null}catch{return globalThis.document?.referrer||null}}function v(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var C=typeof globalThis<"u"&&typeof globalThis.document<"u",w=class{constructor(e){this.evalResult=null;this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.initialized=!1;this.beforeUnloadHandler=null;this.lastEtag=null;this.eventSource=null;this.deviceId=null;this.utmParams={};this.initialReferrer=null;this.pageviewCleanup=null;this.trackedExposures=new Set;this.config={apiKey:e.apiKey,baseUrl:e.baseUrl.replace(/\/$/,""),ingestUrl:(e.ingestUrl||e.baseUrl).replace(/\/$/,""),distinctId:e.distinctId??null,attributes:e.attributes||{},autoFlushInterval:e.autoFlushInterval??3e4,autoFlushSize:e.autoFlushSize??20,enableLocalEvaluation:e.enableLocalEvaluation??!0,configRefreshInterval:e.configRefreshInterval??3e4,realtimeUpdates:e.realtimeUpdates??!1,onConfigUpdate:e.onConfigUpdate??null,captureContext:e.captureContext??C,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{},environment:e.environment??"production"},this.config.captureUtm&&(this.utmParams=P()),this.config.captureContext&&(this.initialReferrer=$()),this.deviceId=L()}get effectiveDistinctId(){return this.config.distinctId||this.deviceId}get bucketingId(){return this.deviceId}async initialize(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=t,this.evalResult=b(this.serverConfig,this.bucketingId,this.config.attributes)}catch(e){this.serverConfig||(console.warn("SplitLab: config fetch failed, using safe defaults",e),this.serverConfig={experiments:[],flags:[]},this.evalResult={experiments:{},flags:{}})}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.autoFlushInterval),this.config.enableLocalEvaluation&&(this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.config.configRefreshInterval)),this.config.realtimeUpdates&&this.config.enableLocalEvaluation&&this.connectConfigStream(),C&&(this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&C&&this.setupPageviewTracking(),this.initialized=!0}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");let t=this.evalResult.experiments[e]??null;return t!==null&&!this.trackedExposures.has(e)&&(this.trackedExposures.add(e),this.track("$exposure",{experiment_key:e,variant_key:t}).catch(()=>{})),t}isFeatureEnabled(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.flags[e]??!1}async track(e,t){let i=this.enrichProperties(t);this.eventQueue.push({distinct_id:this.effectiveDistinctId,event_name:e,properties:i,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.config.autoFlushSize&&await this.flush()}async trackPageview(e){await this.track("$pageview",e)}setSuperProperties(e){Object.assign(this.config.superProperties,e)}unsetSuperProperty(e){delete this.config.superProperties[e]}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{await this.request("POST","/ingest/batch",{events:e},!0)}catch{this.eventQueue=e.concat(this.eventQueue)}}async identify(e,t){let i=this.effectiveDistinctId;this.config.distinctId=e,i!==e&&await this.track("$identify",{...t,previous_distinct_id:i}),this.config.enableLocalEvaluation||(this.evalResult=null,this.initialized=!1,await this.initialize())}async refresh(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t,notModified:i}=await this.fetchConfig();if(i)return;this.serverConfig=e,this.lastEtag=t,this.evalResult=b(this.serverConfig,this.bucketingId,this.config.attributes),this.config.onConfigUpdate&&this.config.onConfigUpdate()}catch{}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}}getDistinctId(){return this.effectiveDistinctId}getDeviceId(){return this.deviceId}setAttributes(e){this.config.attributes={...this.config.attributes,...e}}async destroy(){await this.flush(),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.beforeUnloadHandler&&typeof globalThis.removeEventListener=="function"&&(globalThis.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageviewCleanup&&(this.pageviewCleanup(),this.pageviewCleanup=null),this.initialized=!1}enrichProperties(e){let t={};if(this.config.captureContext){let i=x();if(i){for(let[a,r]of Object.entries(i))r!==null&&r!==""&&r!==void 0&&(t[a]=r);this.initialReferrer&&(t.referrer=this.initialReferrer)}}if(this.config.captureUtm)for(let[i,a]of Object.entries(this.utmParams))a&&(t[i]=a);this.config.trackSessions&&(t.session_id=A(this.config.sessionTimeout)),this.config.persistDeviceId&&this.deviceId&&(t.device_id=this.deviceId);for(let[i,a]of Object.entries(this.config.superProperties))a!==void 0&&(t[i]=a);if(e)for(let[i,a]of Object.entries(e))a!==void 0&&(t[i]=a);return t}setupPageviewTracking(){let e=globalThis,t=e.history;if(!t?.pushState)return;let i=e.location?.href,a=t.pushState.bind(t);t.pushState=(...u)=>{a(...u),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...u)=>{r(...u),this.onUrlChange(i),i=e.location?.href};let o=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",o),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=a,t.replaceState=r,e.removeEventListener("popstate",o)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}async fetchConfig(){let e={};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",i=await fetch(`${this.config.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.config.apiKey)}${t}`,{method:"GET",headers:e});if(i.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!i.ok){let o=await i.text().catch(()=>"");throw new Error(`SplitLab API error ${i.status}: ${o}`)}let a=i.headers.get("etag");return{config:await i.json(),etag:a}}connectConfigStream(){if(typeof EventSource>"u")return;let e=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",t=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}${e}`;this.eventSource=new EventSource(t),this.eventSource.addEventListener("config_updated",()=>{this.refresh().catch(()=>{})}),this.eventSource.onerror=()=>{}}flushSync(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];let t=JSON.stringify({events:e}),i=`${this.config.ingestUrl}/ingest/batch`;if(typeof globalThis.navigator?.sendBeacon=="function"){let a=new Blob([t],{type:"application/json"});try{if(globalThis.navigator.sendBeacon(i,a))return}catch{}}if(typeof fetch=="function")try{fetch(i,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:t,keepalive:!0}).catch(()=>{})}catch{}}async request(e,t,i,a){let r=a?this.config.ingestUrl:this.config.baseUrl,o=await fetch(`${r}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!o.ok){let u=await o.text().catch(()=>"");throw new Error(`SplitLab API error ${o.status}: ${u}`)}return o.json()}};return Q(J);})();
1
+ "use strict";var SplitLab=(()=>{var y=Object.defineProperty;var j=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var K=Object.prototype.hasOwnProperty;var H=(n,e)=>{for(var t in e)y(n,t,{get:e[t],enumerable:!0})},Q=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let a of N(e))!K.call(n,a)&&a!==t&&y(n,a,{get:()=>e[a],enumerable:!(i=j(e,a))||i.enumerable});return n};var G=n=>Q(y({},"__esModule",{value:!0}),n);var X={};H(X,{SplitLabClient:()=>k,hashToFloat:()=>E,murmurhash3:()=>g});function g(n,e=0){let t=e>>>0,i=n.length,a=i>>2,r=3432918353,o=461845907;for(let l=0;l<a;l++){let c=n.charCodeAt(l*4)&255|(n.charCodeAt(l*4+1)&255)<<8|(n.charCodeAt(l*4+2)&255)<<16|(n.charCodeAt(l*4+3)&255)<<24;c=Math.imul(c,r),c=c<<15|c>>>17,c=Math.imul(c,o),t^=c,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196}let u=a*4,s=0;switch(i&3){case 3:s^=(n.charCodeAt(u+2)&255)<<16;case 2:s^=(n.charCodeAt(u+1)&255)<<8;case 1:s^=n.charCodeAt(u)&255,s=Math.imul(s,r),s=s<<15|s>>>17,s=Math.imul(s,o),t^=s}return t^=i,t^=t>>>16,t=Math.imul(t,2246822507),t^=t>>>13,t=Math.imul(t,3266489909),t^=t>>>16,t>>>0}function E(n,e){return g(n,e)/4294967295}function S(n,e,t={}){let i={},a={},r=n.segments||[],o=n.exclusion_groups||[],u=new Map;for(let s of o)for(let[l,c]of Object.entries(s.experiments))u.set(l,{layer_key:s.layer_key,...c});for(let s of n.experiments){if(s.targeting_rules&&!v(s.targeting_rules,t,r)){i[s.key]=null;continue}let l=u.get(s.key);if(l){let f=g(l.layer_key+":"+e)%1e4;if(f<l.bucket_start||f>=l.bucket_end){i[s.key]=null;continue}}let c=g(s.key+":"+e);if(c%1e4/100>=s.traffic_percentage){i[s.key]=null;continue}let z=s.variants.reduce((f,F)=>f+F.weight,0),B=c%z,I=0,d=null;for(let f of s.variants)if(I+=f.weight,B<I){d=f.key;break}d||(d=s.variants[s.variants.length-1].key),i[s.key]=d}for(let s of n.flags){if(s.rules&&!v(s.rules,t,r)){a[s.key]=!1;continue}let l=g(s.key+":"+e)%100;a[s.key]=l<s.rollout_percentage}return{experiments:i,flags:a}}function v(n,e,t=[]){if(!n.groups||n.groups.length===0)return!0;let i=r=>{if(!r.attribute)return!0;if(r.attribute==="$segment"){if(!Array.isArray(r.value))return!1;let u=r.value.map(String);return r.operator==="in"?u.some(s=>{let l=t.find(c=>c.id===s||c.key===s);return l?v(l.rules,e,t):!1}):r.operator==="not_in"?!u.some(s=>{let l=t.find(c=>c.id===s||c.key===s);return l?v(l.rules,e,t):!1}):!1}let o=e[r.attribute];if(o==null)return!1;switch(r.operator){case"is":return String(o)===String(r.value);case"is_not":return String(o)!==String(r.value);case"contains":return String(o).toLowerCase().includes(String(r.value).toLowerCase());case"not_contains":return!String(o).toLowerCase().includes(String(r.value).toLowerCase());case"gt":return Number(o)>Number(r.value);case"lt":return Number(o)<Number(r.value);case"gte":return Number(o)>=Number(r.value);case"lte":return Number(o)<=Number(r.value);case"in":return Array.isArray(r.value)&&r.value.map(String).includes(String(o));case"not_in":return Array.isArray(r.value)&&!r.value.map(String).includes(String(o));default:return!1}},a=r=>r.conditions.every(i);return n.match==="any"?n.groups.some(a):n.groups.every(a)}var V=[[/Edg(?:e|A|iOS)?\/(\S+)/,"Edge"],[/OPR\/(\S+)/,"Opera"],[/SamsungBrowser\/(\S+)/,"Samsung Internet"],[/UCBrowser\/(\S+)/,"UC Browser"],[/Firefox\/(\S+)/,"Firefox"],[/CriOS\/(\S+)/,"Chrome"],[/FxiOS\/(\S+)/,"Firefox"],[/Chrome\/(\S+)/,"Chrome"],[/Safari\/(\S+)/,"Safari"]],Y=[[/Windows NT (\d+\.\d+)/,"Windows"],[/Mac OS X ([\d_]+)/,"macOS"],[/Android ([\d.]+)/,"Android"],[/iPhone OS ([\d_]+)/,"iOS"],[/iPad.*OS ([\d_]+)/,"iPadOS"],[/CrOS/,"ChromeOS"],[/Linux/,"Linux"]];function T(n){if(!n)return{browser:null,browser_version:null,os:null,os_version:null,device_type:null};let e=null,t=null,i=null,a=null;if(/Safari/.test(n)&&!/Chrome|CriOS|Chromium|Edg|OPR|SamsungBrowser|UCBrowser/.test(n)){let o=n.match(/Version\/(\S+)/);e="Safari",t=o?o[1]:null}else for(let[o,u]of V){let s=n.match(o);if(s){e=u,t=s[1]||null;break}}for(let[o,u]of Y){let s=n.match(o);if(s){i=u,a=s[1]?.replace(/_/g,".")||null;break}}let r="desktop";return/Mobi|Android.*Mobile|iPhone/.test(n)?r="mobile":/iPad|Android(?!.*Mobile)|Tablet/.test(n)&&(r="tablet"),{browser:e,browser_version:t,os:i,os_version:a,device_type:r}}var h=typeof globalThis<"u"&&typeof globalThis.document<"u";function P(){if(!h)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,a=n.screen,r=T(e?.userAgent);return{pathname:t?.pathname??null,hostname:t?.hostname??null,referrer:i?.referrer||null,search_params:t?.search||null,page_title:i?.title||null,hash:t?.hash||null,user_agent:e?.userAgent??null,browser:r.browser,browser_version:r.browser_version,os:r.os,os_version:r.os_version,device_type:r.device_type,screen_width:a?.width??null,screen_height:a?.height??null,viewport_width:n.innerWidth??null,viewport_height:n.innerHeight??null,language:e?.language??null,timezone:q()}}function q(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var J=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],x="__ot_utm";function O(){if(!h)return{};try{let i=globalThis.sessionStorage?.getItem(x);if(i)return JSON.parse(i)}catch{}let n=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of J){let a=n.get(i);a&&(e[i]=a,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(x,JSON.stringify(e))}catch{}return e}var p="_sl_did",C="_sl_sid",A=63072e3,U="__ot_did";function w(n){if(!h)return null;try{let e=globalThis.document?.cookie?.match(new RegExp("(?:^|; )"+n.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+"=([^;]*)"));return e?decodeURIComponent(e[1]):null}catch{return null}}function m(n,e,t){if(h)try{globalThis.document.cookie=`${n}=${encodeURIComponent(e)}; path=/; max-age=${t}; SameSite=Lax`}catch{}}function L(n){if(!h)return b();let e=Math.floor(n/1e3),t=w(C);if(t)return m(C,t,e),t;let i=b();return m(C,i,e),i}function $(){if(!h)return b();let n=w(p);if(n)return n;let e=null;try{let t=globalThis.localStorage;e=t?.getItem(U)||null,e&&t?.removeItem(U)}catch{}return e||(e=b()),m(p,e,A),e}function D(n){!h||w(p)===n||m(p,n,A)}var R="__ot_ref";function M(){if(!h)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(R);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(R,t),t||null}catch{return globalThis.document?.referrer||null}}function b(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var _=typeof globalThis<"u"&&typeof globalThis.document<"u",k=class{constructor(e){this.evalResult=null;this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.initialized=!1;this.visibilityHandler=null;this.beforeUnloadHandler=null;this.lastEtag=null;this.eventSource=null;this.deviceId=null;this.utmParams={};this.initialReferrer=null;this.pageviewCleanup=null;this.trackedExposures=new Set;this.config={apiKey:e.apiKey,baseUrl:e.baseUrl.replace(/\/$/,""),ingestUrl:(e.ingestUrl||e.baseUrl).replace(/\/$/,""),distinctId:e.distinctId??null,attributes:e.attributes||{},autoFlushInterval:e.autoFlushInterval??8e3,autoFlushSize:e.autoFlushSize??20,enableLocalEvaluation:e.enableLocalEvaluation??!0,realtimeUpdates:e.realtimeUpdates??!1,onConfigUpdate:e.onConfigUpdate??null,captureContext:e.captureContext??_,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{},environment:e.environment??"production"},this.config.captureUtm&&(this.utmParams=O()),this.config.captureContext&&(this.initialReferrer=M()),e.bootstrap?.deviceId?(this.deviceId=e.bootstrap.deviceId,D(e.bootstrap.deviceId)):this.deviceId=$()}get effectiveDistinctId(){return this.config.distinctId||this.deviceId}get bucketingId(){return this.deviceId}async initialize(){let e=this.initialized;if(!e)if(this.config.enableLocalEvaluation)try{let{config:t,etag:i}=await this.fetchConfig();this.serverConfig=t,this.lastEtag=i,this.evalResult=S(this.serverConfig,this.bucketingId,this.config.attributes)}catch(t){this.serverConfig||(console.warn("SplitLab: config fetch failed, using safe defaults",t),this.serverConfig={experiments:[],flags:[]},this.evalResult={experiments:{},flags:{}})}else{let t={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(t.attributes=this.config.attributes);let i=await this.request("POST","/api/sdk/evaluate",t);this.evalResult={experiments:i.experiments,flags:i.flags}}this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.config.autoFlushInterval),this.config.realtimeUpdates&&this.config.enableLocalEvaluation&&this.connectConfigStream(),_&&(this.visibilityHandler=()=>{globalThis.document?.visibilityState==="hidden"?this.flushSync():this.config.enableLocalEvaluation&&this.refresh().catch(()=>{})},globalThis.document?.addEventListener("visibilitychange",this.visibilityHandler),this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&_&&this.setupPageviewTracking(),this.initialized=!0,e&&this.refresh().catch(()=>{})}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");let t=this.evalResult.experiments[e]??null;return t!==null&&!this.trackedExposures.has(e)&&(this.trackedExposures.add(e),this.track("$exposure",{experiment_key:e,variant_key:t}).catch(()=>{})),t}isFeatureEnabled(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.flags[e]??!1}hydrateFromBootstrap(e){this.serverConfig=e.serverConfig,this.lastEtag=e.etag,this.evalResult=e.evalResult,this.initialized=!0}async track(e,t){let i=this.enrichProperties(t);this.eventQueue.push({distinct_id:this.effectiveDistinctId,event_name:e,properties:i,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.config.autoFlushSize&&await this.flush()}async trackPageview(e){await this.track("$pageview",e)}setSuperProperties(e){Object.assign(this.config.superProperties,e)}unsetSuperProperty(e){delete this.config.superProperties[e]}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{await this.request("POST","/ingest/batch",{events:e},!0)}catch{this.eventQueue=e.concat(this.eventQueue)}}async identify(e,t){let i=this.effectiveDistinctId;this.config.distinctId=e,i!==e&&await this.track("$identify",{...t,previous_distinct_id:i}),this.config.enableLocalEvaluation||(this.evalResult=null,this.initialized=!1,await this.initialize())}async refresh(){if(this.config.enableLocalEvaluation)try{let{config:e,etag:t,notModified:i}=await this.fetchConfig();if(i)return;this.serverConfig=e,this.lastEtag=t,this.evalResult=S(this.serverConfig,this.bucketingId,this.config.attributes),this.config.onConfigUpdate&&this.config.onConfigUpdate()}catch{}else{let e={distinct_id:this.effectiveDistinctId};Object.keys(this.config.attributes).length>0&&(e.attributes=this.config.attributes);let t=await this.request("POST","/api/sdk/evaluate",e);this.evalResult={experiments:t.experiments,flags:t.flags}}}getDistinctId(){return this.effectiveDistinctId}getDeviceId(){return this.deviceId}setAttributes(e){this.config.attributes={...this.config.attributes,...e}}async destroy(){await this.flush(),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),this.eventSource&&(this.eventSource.close(),this.eventSource=null),this.visibilityHandler&&(globalThis.document?.removeEventListener("visibilitychange",this.visibilityHandler),this.visibilityHandler=null),this.beforeUnloadHandler&&typeof globalThis.removeEventListener=="function"&&(globalThis.removeEventListener("beforeunload",this.beforeUnloadHandler),this.beforeUnloadHandler=null),this.pageviewCleanup&&(this.pageviewCleanup(),this.pageviewCleanup=null),this.initialized=!1}enrichProperties(e){let t={};if(this.config.captureContext){let i=P();if(i){for(let[a,r]of Object.entries(i))r!==null&&r!==""&&r!==void 0&&(t[a]=r);this.initialReferrer&&(t.referrer=this.initialReferrer)}}if(this.config.captureUtm)for(let[i,a]of Object.entries(this.utmParams))a&&(t[i]=a);this.config.trackSessions&&(t.session_id=L(this.config.sessionTimeout)),this.config.persistDeviceId&&this.deviceId&&(t.device_id=this.deviceId);for(let[i,a]of Object.entries(this.config.superProperties))a!==void 0&&(t[i]=a);if(e)for(let[i,a]of Object.entries(e))a!==void 0&&(t[i]=a);return t}setupPageviewTracking(){let e=globalThis,t=e.history;if(!t?.pushState)return;let i=e.location?.href,a=t.pushState.bind(t);t.pushState=(...u)=>{a(...u),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...u)=>{r(...u),this.onUrlChange(i),i=e.location?.href};let o=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",o),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=a,t.replaceState=r,e.removeEventListener("popstate",o)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}async fetchConfig(){let e={};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",i=await fetch(`${this.config.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.config.apiKey)}${t}`,{method:"GET",headers:e});if(i.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!i.ok){let o=await i.text().catch(()=>"");throw new Error(`SplitLab API error ${i.status}: ${o}`)}let a=i.headers.get("etag");return{config:await i.json(),etag:a}}connectConfigStream(){if(typeof EventSource>"u")return;let e=this.config.environment!=="production"?`&env=${encodeURIComponent(this.config.environment)}`:"",t=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}${e}`;this.eventSource=new EventSource(t),this.eventSource.addEventListener("config_updated",()=>{this.refresh().catch(()=>{})}),this.eventSource.onerror=()=>{}}flushSync(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];let t=JSON.stringify({events:e}),i=`${this.config.ingestUrl}/ingest/batch?key=${encodeURIComponent(this.config.apiKey)}`;if(typeof globalThis.navigator?.sendBeacon=="function"){let a=new Blob([t],{type:"application/json"});try{if(globalThis.navigator.sendBeacon(i,a))return}catch{}}if(typeof fetch=="function")try{fetch(i,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:t,keepalive:!0}).catch(()=>{})}catch{}}async request(e,t,i,a){let r=a?this.config.ingestUrl:this.config.baseUrl,o=await fetch(`${r}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!o.ok){let u=await o.text().catch(()=>"");throw new Error(`SplitLab API error ${o.status}: ${u}`)}return o.json()}};return G(X);})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitlab/js-client",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "description": "Lightweight JavaScript client SDK for SplitLab A/B testing and analytics",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -30,7 +30,7 @@
30
30
  "dev": "tsup --watch"
31
31
  },
32
32
  "dependencies": {
33
- "@splitlab/core": "^0.3.0"
33
+ "@splitlab/core": "^0.3.2"
34
34
  },
35
35
  "devDependencies": {
36
36
  "tsup": "^8.0.0",