@splitlab/node 0.4.0 → 0.5.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 m=Object.defineProperty;var E=Object.getOwnPropertyDescriptor;var b=Object.getOwnPropertyNames;var k=Object.prototype.hasOwnProperty;var S=(n,e)=>{for(var s in e)m(n,s,{get:e[s],enumerable:!0})},x=(n,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of b(e))!k.call(n,r)&&r!==s&&m(n,r,{get:()=>e[r],enumerable:!(t=E(e,r))||t.enumerable});return n};var U=n=>x(m({},"__esModule",{value:!0}),n);var M={};S(M,{RequestContext:()=>p,SplitLabServer:()=>_,hashToFloat:()=>d.hashToFloat,murmurhash3:()=>d.murmurhash3});module.exports=U(M);var d=require("@splitlab/core"),l=require("@splitlab/core"),v=class v{constructor(e){this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.lastEtag=null;this.ready=!1;this.trackedExposures=new Set;this.apiKey=e.apiKey,this.baseUrl=e.baseUrl.replace(/\/$/,""),this.ingestUrl=(e.ingestUrl||e.baseUrl).replace(/\/$/,""),this.configRefreshInterval=e.configRefreshInterval??3e4,this.flushInterval=e.flushInterval??1e4,this.flushSize=e.flushSize??100,this.onConfigUpdate=e.onConfigUpdate??null,this.environment=e.environment??"production"}async initialize(){let{config:e,etag:s}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=s,this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.configRefreshInterval),this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.flushInterval),this.ready=!0}isReady(){return this.ready}async destroy(){this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),await this.flush(),this.ready=!1}getVariant(e,s,t){if(!this.serverConfig)return null;let r=this.serverConfig.experiments.find(o=>o.key===e);if(!r)return null;let i=this.serverConfig.segments||[];if(r.targeting_rules&&!(0,l.evaluateRules)(r.targeting_rules,t||{},i))return null;let a=(0,l.murmurhash3)(r.key+":"+s);if(a%1e4/100>=r.traffic_percentage)return null;let h=r.variants.reduce((o,R)=>o+R.weight,0),f=a%h,c=0;for(let o of r.variants)if(c+=o.weight,f<c)return this.trackExposure(e,o.key,s),o.key;let u=r.variants[r.variants.length-1].key;return this.trackExposure(e,u,s),u}isFeatureEnabled(e,s,t){if(!this.serverConfig)return!1;let r=this.serverConfig.flags.find(h=>h.key===e);if(!r)return!1;let i=this.serverConfig.segments||[];return r.rules&&!(0,l.evaluateRules)(r.rules,t||{},i)?!1:(0,l.murmurhash3)(r.key+":"+s)%100<r.rollout_percentage}evaluateAll(e,s){return this.serverConfig?(0,l.localEvaluate)(this.serverConfig,e,s||{}):{experiments:{},flags:{}}}withContext(e){return new p(this,e)}track(e,s,t){this.eventQueue.push({distinct_id:e,event_name:s,properties:t,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.flushSize&&this.flush().catch(()=>{})}trackExposure(e,s,t){let r=`${e}:${t}`;this.trackedExposures.has(r)||(this.trackedExposures.size>=v.MAX_EXPOSURE_CACHE&&this.trackedExposures.clear(),this.trackedExposures.add(r),this.track(t,"$exposure",{experiment_key:e,variant_key:s}))}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{(await fetch(`${this.ingestUrl}/ingest/batch`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.apiKey},body:JSON.stringify({events:e})})).ok||(this.eventQueue=e.concat(this.eventQueue))}catch{this.eventQueue=e.concat(this.eventQueue)}}async refresh(){try{let{config:e,etag:s,notModified:t}=await this.fetchConfig();if(t)return;this.serverConfig=e,this.lastEtag=s,this.onConfigUpdate&&this.onConfigUpdate()}catch{}}async fetchConfig(){let e={"Content-Type":"application/json"};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let s=this.environment!=="production"?`&env=${encodeURIComponent(this.environment)}`:"",t=await fetch(`${this.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.apiKey)}${s}`,{method:"GET",headers:e});if(t.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!t.ok){let a=await t.text().catch(()=>"");throw new Error(`SplitLab API error ${t.status}: ${a}`)}let r=t.headers.get("etag");return{config:await t.json(),etag:r}}};v.MAX_EXPOSURE_CACHE=5e4;var _=v,y="_sl_did",I="_sl_sid",w=63072e3,T=1800,P=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],p=class{constructor(e,s){this.server=e;let t=$(s.headers),r=s.cookies||A(t.cookie||""),i=(0,l.parseUserAgent)(t["user-agent"]||void 0),a=O(s.url,t.host),g=r[y]||null;this._deviceId=g||C(),this._newDeviceId=!g;let h=r[I]||null;this._sessionId=h||C(),this._newSessionId=!h,this.props={},i.browser&&(this.props.browser=i.browser),i.browser_version&&(this.props.browser_version=i.browser_version),i.os&&(this.props.os=i.os),i.os_version&&(this.props.os_version=i.os_version),i.device_type&&(this.props.device_type=i.device_type),t["user-agent"]&&(this.props.user_agent=t["user-agent"]),a.pathname&&(this.props.pathname=a.pathname),a.hostname&&(this.props.hostname=a.hostname);let f=t.referer||t.referrer||null;f&&(this.props.referrer=f);let c=t["accept-language"];if(c&&(this.props.language=c.split(",")[0].split(";")[0].trim()),a.searchParams)for(let u of P){let o=a.searchParams.get(u);o&&(this.props[u]=o)}this.props.device_id=this._deviceId,this._sessionId&&(this.props.session_id=this._sessionId)}get deviceId(){return this._deviceId}get sessionId(){return this._sessionId}getVariant(e,s){return this.server.getVariant(e,s||this._deviceId)}isFeatureEnabled(e,s,t){return this.server.isFeatureEnabled(e,s||this._deviceId,t)}track(e,s,t){this.server.track(t||this._deviceId,e,{...this.props,...s})}getResponseCookies(){let e=[];return this._newDeviceId&&e.push(`${y}=${encodeURIComponent(this._deviceId)}; Path=/; Max-Age=${w}; SameSite=Lax`),this._sessionId&&e.push(`${I}=${encodeURIComponent(this._sessionId)}; Path=/; Max-Age=${T}; SameSite=Lax`),e}};function C(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}function $(n){let e={};if(n instanceof Headers)n.forEach((s,t)=>{e[t]=s});else for(let[s,t]of Object.entries(n))t!==void 0&&(e[s.toLowerCase()]=Array.isArray(t)?t[0]:t);return e}function A(n){let e={};if(!n)return e;for(let s of n.split(";")){let t=s.indexOf("=");if(t===-1)continue;let r=s.substring(0,t).trim(),i=s.substring(t+1).trim();try{e[r]=decodeURIComponent(i)}catch{e[r]=i}}return e}function O(n,e){if(!n)return{pathname:null,hostname:null,searchParams:null};try{let s=e?`http://${e}`:"http://localhost",t=new URL(n,s);return{pathname:t.pathname,hostname:t.hostname!=="localhost"?t.hostname:e?.split(":")[0]||null,searchParams:t.searchParams}}catch{return{pathname:n.split("?")[0]||null,hostname:null,searchParams:null}}}0&&(module.exports={RequestContext,SplitLabServer,hashToFloat,murmurhash3});
1
+ "use strict";var m=Object.defineProperty;var R=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var b=Object.prototype.hasOwnProperty;var k=(n,e)=>{for(var s in e)m(n,s,{get:e[s],enumerable:!0})},x=(n,e,s,t)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of S(e))!b.call(n,r)&&r!==s&&m(n,r,{get:()=>e[r],enumerable:!(t=R(e,r))||t.enumerable});return n};var w=n=>x(m({},"__esModule",{value:!0}),n);var D={};k(D,{RequestContext:()=>p,SplitLabServer:()=>_,hashToFloat:()=>d.hashToFloat,murmurhash3:()=>d.murmurhash3});module.exports=w(D);var d=require("@splitlab/core"),l=require("@splitlab/core"),v=class v{constructor(e){this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.lastEtag=null;this.ready=!1;this.trackedExposures=new Set;this.apiKey=e.apiKey,this.baseUrl=e.baseUrl.replace(/\/$/,""),this.ingestUrl=(e.ingestUrl||e.baseUrl).replace(/\/$/,""),this.configRefreshInterval=e.configRefreshInterval??3e4,this.flushInterval=e.flushInterval??1e4,this.flushSize=e.flushSize??100,this.onConfigUpdate=e.onConfigUpdate??null,this.environment=e.environment??"production"}async initialize(){let{config:e,etag:s}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=s,this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.configRefreshInterval),this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.flushInterval),this.ready=!0}isReady(){return this.ready}async destroy(){this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),await this.flush(),this.ready=!1}getVariant(e,s,t){if(!this.serverConfig)return null;let r=this.serverConfig.experiments.find(o=>o.key===e);if(!r)return null;let i=this.serverConfig.segments||[];if(r.targeting_rules&&!(0,l.evaluateRules)(r.targeting_rules,t||{},i))return null;let a=(0,l.murmurhash3)(r.key+":"+s);if(a%1e4/100>=r.traffic_percentage)return null;let h=r.variants.reduce((o,E)=>o+E.weight,0),f=a%h,c=0;for(let o of r.variants)if(c+=o.weight,f<c)return this.trackExposure(e,o.key,s),o.key;let g=r.variants[r.variants.length-1].key;return this.trackExposure(e,g,s),g}isFeatureEnabled(e,s,t){if(!this.serverConfig)return!1;let r=this.serverConfig.flags.find(h=>h.key===e);if(!r)return!1;let i=this.serverConfig.segments||[];return r.rules&&!(0,l.evaluateRules)(r.rules,t||{},i)?!1:(0,l.murmurhash3)(r.key+":"+s)%100<r.rollout_percentage}evaluateAll(e,s){return this.serverConfig?(0,l.localEvaluate)(this.serverConfig,e,s||{}):{experiments:{},flags:{}}}getConfigSnapshot(){return{serverConfig:this.serverConfig,lastEtag:this.lastEtag}}withContext(e){return new p(this,e)}track(e,s,t){this.eventQueue.push({distinct_id:e,event_name:s,properties:t,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.flushSize&&this.flush().catch(()=>{})}trackExposure(e,s,t){let r=`${e}:${t}`;this.trackedExposures.has(r)||(this.trackedExposures.size>=v.MAX_EXPOSURE_CACHE&&this.trackedExposures.clear(),this.trackedExposures.add(r),this.track(t,"$exposure",{experiment_key:e,variant_key:s}))}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{(await fetch(`${this.ingestUrl}/ingest/batch`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.apiKey},body:JSON.stringify({events:e})})).ok||(this.eventQueue=e.concat(this.eventQueue))}catch{this.eventQueue=e.concat(this.eventQueue)}}async refresh(){try{let{config:e,etag:s,notModified:t}=await this.fetchConfig();if(t)return;this.serverConfig=e,this.lastEtag=s,this.onConfigUpdate&&this.onConfigUpdate()}catch{}}async fetchConfig(){let e={"Content-Type":"application/json"};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let s=this.environment!=="production"?`&env=${encodeURIComponent(this.environment)}`:"",t=await fetch(`${this.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.apiKey)}${s}`,{method:"GET",headers:e});if(t.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!t.ok){let a=await t.text().catch(()=>"");throw new Error(`SplitLab API error ${t.status}: ${a}`)}let r=t.headers.get("etag");return{config:await t.json(),etag:r}}};v.MAX_EXPOSURE_CACHE=5e4;var _=v,C="_sl_did",I="_sl_sid",U=63072e3,T=1800,P=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],p=class{constructor(e,s){this.server=e;let t=$(s.headers),r=s.cookies||A(t.cookie||""),i=(0,l.parseUserAgent)(t["user-agent"]||void 0),a=O(s.url,t.host),u=r[C]||null;this._deviceId=u||y(),this._newDeviceId=!u;let h=r[I]||null;this._sessionId=h||y(),this._newSessionId=!h,this.props={},i.browser&&(this.props.browser=i.browser),i.browser_version&&(this.props.browser_version=i.browser_version),i.os&&(this.props.os=i.os),i.os_version&&(this.props.os_version=i.os_version),i.device_type&&(this.props.device_type=i.device_type),t["user-agent"]&&(this.props.user_agent=t["user-agent"]),a.pathname&&(this.props.pathname=a.pathname),a.hostname&&(this.props.hostname=a.hostname);let f=t.referer||t.referrer||null;f&&(this.props.referrer=f);let c=t["accept-language"];if(c&&(this.props.language=c.split(",")[0].split(";")[0].trim()),a.searchParams)for(let g of P){let o=a.searchParams.get(g);o&&(this.props[g]=o)}this.props.device_id=this._deviceId,this._sessionId&&(this.props.session_id=this._sessionId)}get deviceId(){return this._deviceId}get sessionId(){return this._sessionId}getVariant(e,s){return this.server.getVariant(e,s||this._deviceId)}isFeatureEnabled(e,s,t){return this.server.isFeatureEnabled(e,s||this._deviceId,t)}track(e,s,t){this.server.track(t||this._deviceId,e,{...this.props,...s})}getResponseCookies(){let e=[];return this._newDeviceId&&e.push(`${C}=${encodeURIComponent(this._deviceId)}; Path=/; Max-Age=${U}; SameSite=Lax`),this._sessionId&&e.push(`${I}=${encodeURIComponent(this._sessionId)}; Path=/; Max-Age=${T}; SameSite=Lax`),e}getBootstrapData(e,s){let t=e||this._deviceId,r=this.server.evaluateAll(t,s),{serverConfig:i,lastEtag:a}=this.server.getConfigSnapshot();return{evalResult:r,serverConfig:i||{experiments:[],flags:[]},deviceId:this._deviceId,sessionId:this._sessionId,etag:a,generatedAt:new Date().toISOString()}}};function y(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}function $(n){let e={};if(n instanceof Headers)n.forEach((s,t)=>{e[t]=s});else for(let[s,t]of Object.entries(n))t!==void 0&&(e[s.toLowerCase()]=Array.isArray(t)?t[0]:t);return e}function A(n){let e={};if(!n)return e;for(let s of n.split(";")){let t=s.indexOf("=");if(t===-1)continue;let r=s.substring(0,t).trim(),i=s.substring(t+1).trim();try{e[r]=decodeURIComponent(i)}catch{e[r]=i}}return e}function O(n,e){if(!n)return{pathname:null,hostname:null,searchParams:null};try{let s=e?`http://${e}`:"http://localhost",t=new URL(n,s);return{pathname:t.pathname,hostname:t.hostname!=="localhost"?t.hostname:e?.split(":")[0]||null,searchParams:t.searchParams}}catch{return{pathname:n.split("?")[0]||null,hostname:null,searchParams:null}}}0&&(module.exports={RequestContext,SplitLabServer,hashToFloat,murmurhash3});
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { EvalResult } from '@splitlab/core';
2
- export { EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, UAResult, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
1
+ import { EvalResult, ServerConfig, BootstrapData } from '@splitlab/core';
2
+ export { BootstrapData, EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, UAResult, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
3
3
 
4
4
  interface SplitLabServerConfig {
5
5
  apiKey: string;
@@ -40,6 +40,10 @@ declare class SplitLabServer {
40
40
  getVariant(experimentKey: string, distinctId: string, attributes?: Record<string, any>): string | null;
41
41
  isFeatureEnabled(flagKey: string, distinctId: string, attributes?: Record<string, any>): boolean;
42
42
  evaluateAll(distinctId: string, attributes?: Record<string, any>): EvalResult;
43
+ getConfigSnapshot(): {
44
+ serverConfig: ServerConfig | null;
45
+ lastEtag: string | null;
46
+ };
43
47
  /**
44
48
  * Create a request-scoped context that enriches events with server-side
45
49
  * properties parsed from the HTTP request (UA, URL, cookies, UTM, etc.).
@@ -84,6 +88,12 @@ declare class RequestContext {
84
88
  * Sets `_sl_did` (2yr) if new, always refreshes `_sl_sid` (30min sliding).
85
89
  */
86
90
  getResponseCookies(): string[];
91
+ /**
92
+ * Generate bootstrap data for client-side hydration.
93
+ * Evaluates all experiments/flags (pure, no exposure events) and packages
94
+ * the result with device ID, session ID, server config, and ETag.
95
+ */
96
+ getBootstrapData(distinctId?: string, attributes?: Record<string, any>): BootstrapData;
87
97
  }
88
98
 
89
99
  export { type IncomingRequest, RequestContext, SplitLabServer, type SplitLabServerConfig };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { EvalResult } from '@splitlab/core';
2
- export { EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, UAResult, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
1
+ import { EvalResult, ServerConfig, BootstrapData } from '@splitlab/core';
2
+ export { BootstrapData, EvalResult, ExclusionGroupConfig, ExperimentConfig, FlagConfig, SegmentConfig, ServerConfig, TargetingCondition, TargetingGroup, TargetingRules, TrackEvent, UAResult, Variant, hashToFloat, murmurhash3 } from '@splitlab/core';
3
3
 
4
4
  interface SplitLabServerConfig {
5
5
  apiKey: string;
@@ -40,6 +40,10 @@ declare class SplitLabServer {
40
40
  getVariant(experimentKey: string, distinctId: string, attributes?: Record<string, any>): string | null;
41
41
  isFeatureEnabled(flagKey: string, distinctId: string, attributes?: Record<string, any>): boolean;
42
42
  evaluateAll(distinctId: string, attributes?: Record<string, any>): EvalResult;
43
+ getConfigSnapshot(): {
44
+ serverConfig: ServerConfig | null;
45
+ lastEtag: string | null;
46
+ };
43
47
  /**
44
48
  * Create a request-scoped context that enriches events with server-side
45
49
  * properties parsed from the HTTP request (UA, URL, cookies, UTM, etc.).
@@ -84,6 +88,12 @@ declare class RequestContext {
84
88
  * Sets `_sl_did` (2yr) if new, always refreshes `_sl_sid` (30min sliding).
85
89
  */
86
90
  getResponseCookies(): string[];
91
+ /**
92
+ * Generate bootstrap data for client-side hydration.
93
+ * Evaluates all experiments/flags (pure, no exposure events) and packages
94
+ * the result with device ID, session ID, server config, and ETag.
95
+ */
96
+ getBootstrapData(distinctId?: string, attributes?: Record<string, any>): BootstrapData;
87
97
  }
88
98
 
89
99
  export { type IncomingRequest, RequestContext, SplitLabServer, type SplitLabServerConfig };
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- import{murmurhash3 as A,hashToFloat as O}from"@splitlab/core";import{murmurhash3 as v,localEvaluate as R,evaluateRules as d,parseUserAgent as E}from"@splitlab/core";var f=class f{constructor(e){this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.lastEtag=null;this.ready=!1;this.trackedExposures=new Set;this.apiKey=e.apiKey,this.baseUrl=e.baseUrl.replace(/\/$/,""),this.ingestUrl=(e.ingestUrl||e.baseUrl).replace(/\/$/,""),this.configRefreshInterval=e.configRefreshInterval??3e4,this.flushInterval=e.flushInterval??1e4,this.flushSize=e.flushSize??100,this.onConfigUpdate=e.onConfigUpdate??null,this.environment=e.environment??"production"}async initialize(){let{config:e,etag:s}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=s,this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.configRefreshInterval),this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.flushInterval),this.ready=!0}isReady(){return this.ready}async destroy(){this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),await this.flush(),this.ready=!1}getVariant(e,s,t){if(!this.serverConfig)return null;let r=this.serverConfig.experiments.find(o=>o.key===e);if(!r)return null;let n=this.serverConfig.segments||[];if(r.targeting_rules&&!d(r.targeting_rules,t||{},n))return null;let a=v(r.key+":"+s);if(a%1e4/100>=r.traffic_percentage)return null;let l=r.variants.reduce((o,C)=>o+C.weight,0),g=a%l,h=0;for(let o of r.variants)if(h+=o.weight,g<h)return this.trackExposure(e,o.key,s),o.key;let c=r.variants[r.variants.length-1].key;return this.trackExposure(e,c,s),c}isFeatureEnabled(e,s,t){if(!this.serverConfig)return!1;let r=this.serverConfig.flags.find(l=>l.key===e);if(!r)return!1;let n=this.serverConfig.segments||[];return r.rules&&!d(r.rules,t||{},n)?!1:v(r.key+":"+s)%100<r.rollout_percentage}evaluateAll(e,s){return this.serverConfig?R(this.serverConfig,e,s||{}):{experiments:{},flags:{}}}withContext(e){return new p(this,e)}track(e,s,t){this.eventQueue.push({distinct_id:e,event_name:s,properties:t,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.flushSize&&this.flush().catch(()=>{})}trackExposure(e,s,t){let r=`${e}:${t}`;this.trackedExposures.has(r)||(this.trackedExposures.size>=f.MAX_EXPOSURE_CACHE&&this.trackedExposures.clear(),this.trackedExposures.add(r),this.track(t,"$exposure",{experiment_key:e,variant_key:s}))}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{(await fetch(`${this.ingestUrl}/ingest/batch`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.apiKey},body:JSON.stringify({events:e})})).ok||(this.eventQueue=e.concat(this.eventQueue))}catch{this.eventQueue=e.concat(this.eventQueue)}}async refresh(){try{let{config:e,etag:s,notModified:t}=await this.fetchConfig();if(t)return;this.serverConfig=e,this.lastEtag=s,this.onConfigUpdate&&this.onConfigUpdate()}catch{}}async fetchConfig(){let e={"Content-Type":"application/json"};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let s=this.environment!=="production"?`&env=${encodeURIComponent(this.environment)}`:"",t=await fetch(`${this.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.apiKey)}${s}`,{method:"GET",headers:e});if(t.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!t.ok){let a=await t.text().catch(()=>"");throw new Error(`SplitLab API error ${t.status}: ${a}`)}let r=t.headers.get("etag");return{config:await t.json(),etag:r}}};f.MAX_EXPOSURE_CACHE=5e4;var m=f,_="_sl_did",y="_sl_sid",b=63072e3,k=1800,S=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],p=class{constructor(e,s){this.server=e;let t=x(s.headers),r=s.cookies||U(t.cookie||""),n=E(t["user-agent"]||void 0),a=w(s.url,t.host),u=r[_]||null;this._deviceId=u||I(),this._newDeviceId=!u;let l=r[y]||null;this._sessionId=l||I(),this._newSessionId=!l,this.props={},n.browser&&(this.props.browser=n.browser),n.browser_version&&(this.props.browser_version=n.browser_version),n.os&&(this.props.os=n.os),n.os_version&&(this.props.os_version=n.os_version),n.device_type&&(this.props.device_type=n.device_type),t["user-agent"]&&(this.props.user_agent=t["user-agent"]),a.pathname&&(this.props.pathname=a.pathname),a.hostname&&(this.props.hostname=a.hostname);let g=t.referer||t.referrer||null;g&&(this.props.referrer=g);let h=t["accept-language"];if(h&&(this.props.language=h.split(",")[0].split(";")[0].trim()),a.searchParams)for(let c of S){let o=a.searchParams.get(c);o&&(this.props[c]=o)}this.props.device_id=this._deviceId,this._sessionId&&(this.props.session_id=this._sessionId)}get deviceId(){return this._deviceId}get sessionId(){return this._sessionId}getVariant(e,s){return this.server.getVariant(e,s||this._deviceId)}isFeatureEnabled(e,s,t){return this.server.isFeatureEnabled(e,s||this._deviceId,t)}track(e,s,t){this.server.track(t||this._deviceId,e,{...this.props,...s})}getResponseCookies(){let e=[];return this._newDeviceId&&e.push(`${_}=${encodeURIComponent(this._deviceId)}; Path=/; Max-Age=${b}; SameSite=Lax`),this._sessionId&&e.push(`${y}=${encodeURIComponent(this._sessionId)}; Path=/; Max-Age=${k}; SameSite=Lax`),e}};function I(){let i=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${i}-${e}`}function x(i){let e={};if(i instanceof Headers)i.forEach((s,t)=>{e[t]=s});else for(let[s,t]of Object.entries(i))t!==void 0&&(e[s.toLowerCase()]=Array.isArray(t)?t[0]:t);return e}function U(i){let e={};if(!i)return e;for(let s of i.split(";")){let t=s.indexOf("=");if(t===-1)continue;let r=s.substring(0,t).trim(),n=s.substring(t+1).trim();try{e[r]=decodeURIComponent(n)}catch{e[r]=n}}return e}function w(i,e){if(!i)return{pathname:null,hostname:null,searchParams:null};try{let s=e?`http://${e}`:"http://localhost",t=new URL(i,s);return{pathname:t.pathname,hostname:t.hostname!=="localhost"?t.hostname:e?.split(":")[0]||null,searchParams:t.searchParams}}catch{return{pathname:i.split("?")[0]||null,hostname:null,searchParams:null}}}export{p as RequestContext,m as SplitLabServer,O as hashToFloat,A as murmurhash3};
1
+ import{murmurhash3 as A,hashToFloat as O}from"@splitlab/core";import{murmurhash3 as v,localEvaluate as E,evaluateRules as d,parseUserAgent as R}from"@splitlab/core";var f=class f{constructor(e){this.serverConfig=null;this.eventQueue=[];this.flushTimer=null;this.configRefreshTimer=null;this.lastEtag=null;this.ready=!1;this.trackedExposures=new Set;this.apiKey=e.apiKey,this.baseUrl=e.baseUrl.replace(/\/$/,""),this.ingestUrl=(e.ingestUrl||e.baseUrl).replace(/\/$/,""),this.configRefreshInterval=e.configRefreshInterval??3e4,this.flushInterval=e.flushInterval??1e4,this.flushSize=e.flushSize??100,this.onConfigUpdate=e.onConfigUpdate??null,this.environment=e.environment??"production"}async initialize(){let{config:e,etag:s}=await this.fetchConfig();this.serverConfig=e,this.lastEtag=s,this.configRefreshTimer=setInterval(()=>{this.refresh().catch(()=>{})},this.configRefreshInterval),this.flushTimer=setInterval(()=>{this.flush().catch(()=>{})},this.flushInterval),this.ready=!0}isReady(){return this.ready}async destroy(){this.configRefreshTimer!==null&&(clearInterval(this.configRefreshTimer),this.configRefreshTimer=null),this.flushTimer!==null&&(clearInterval(this.flushTimer),this.flushTimer=null),await this.flush(),this.ready=!1}getVariant(e,s,t){if(!this.serverConfig)return null;let r=this.serverConfig.experiments.find(o=>o.key===e);if(!r)return null;let n=this.serverConfig.segments||[];if(r.targeting_rules&&!d(r.targeting_rules,t||{},n))return null;let i=v(r.key+":"+s);if(i%1e4/100>=r.traffic_percentage)return null;let l=r.variants.reduce((o,y)=>o+y.weight,0),u=i%l,h=0;for(let o of r.variants)if(h+=o.weight,u<h)return this.trackExposure(e,o.key,s),o.key;let c=r.variants[r.variants.length-1].key;return this.trackExposure(e,c,s),c}isFeatureEnabled(e,s,t){if(!this.serverConfig)return!1;let r=this.serverConfig.flags.find(l=>l.key===e);if(!r)return!1;let n=this.serverConfig.segments||[];return r.rules&&!d(r.rules,t||{},n)?!1:v(r.key+":"+s)%100<r.rollout_percentage}evaluateAll(e,s){return this.serverConfig?E(this.serverConfig,e,s||{}):{experiments:{},flags:{}}}getConfigSnapshot(){return{serverConfig:this.serverConfig,lastEtag:this.lastEtag}}withContext(e){return new p(this,e)}track(e,s,t){this.eventQueue.push({distinct_id:e,event_name:s,properties:t,timestamp:new Date().toISOString()}),this.eventQueue.length>=this.flushSize&&this.flush().catch(()=>{})}trackExposure(e,s,t){let r=`${e}:${t}`;this.trackedExposures.has(r)||(this.trackedExposures.size>=f.MAX_EXPOSURE_CACHE&&this.trackedExposures.clear(),this.trackedExposures.add(r),this.track(t,"$exposure",{experiment_key:e,variant_key:s}))}async flush(){if(this.eventQueue.length===0)return;let e=this.eventQueue;this.eventQueue=[];try{(await fetch(`${this.ingestUrl}/ingest/batch`,{method:"POST",headers:{"Content-Type":"application/json","X-API-Key":this.apiKey},body:JSON.stringify({events:e})})).ok||(this.eventQueue=e.concat(this.eventQueue))}catch{this.eventQueue=e.concat(this.eventQueue)}}async refresh(){try{let{config:e,etag:s,notModified:t}=await this.fetchConfig();if(t)return;this.serverConfig=e,this.lastEtag=s,this.onConfigUpdate&&this.onConfigUpdate()}catch{}}async fetchConfig(){let e={"Content-Type":"application/json"};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let s=this.environment!=="production"?`&env=${encodeURIComponent(this.environment)}`:"",t=await fetch(`${this.baseUrl}/api/sdk/config?key=${encodeURIComponent(this.apiKey)}${s}`,{method:"GET",headers:e});if(t.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!t.ok){let i=await t.text().catch(()=>"");throw new Error(`SplitLab API error ${t.status}: ${i}`)}let r=t.headers.get("etag");return{config:await t.json(),etag:r}}};f.MAX_EXPOSURE_CACHE=5e4;var m=f,_="_sl_did",C="_sl_sid",S=63072e3,b=1800,k=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],p=class{constructor(e,s){this.server=e;let t=x(s.headers),r=s.cookies||w(t.cookie||""),n=R(t["user-agent"]||void 0),i=U(s.url,t.host),g=r[_]||null;this._deviceId=g||I(),this._newDeviceId=!g;let l=r[C]||null;this._sessionId=l||I(),this._newSessionId=!l,this.props={},n.browser&&(this.props.browser=n.browser),n.browser_version&&(this.props.browser_version=n.browser_version),n.os&&(this.props.os=n.os),n.os_version&&(this.props.os_version=n.os_version),n.device_type&&(this.props.device_type=n.device_type),t["user-agent"]&&(this.props.user_agent=t["user-agent"]),i.pathname&&(this.props.pathname=i.pathname),i.hostname&&(this.props.hostname=i.hostname);let u=t.referer||t.referrer||null;u&&(this.props.referrer=u);let h=t["accept-language"];if(h&&(this.props.language=h.split(",")[0].split(";")[0].trim()),i.searchParams)for(let c of k){let o=i.searchParams.get(c);o&&(this.props[c]=o)}this.props.device_id=this._deviceId,this._sessionId&&(this.props.session_id=this._sessionId)}get deviceId(){return this._deviceId}get sessionId(){return this._sessionId}getVariant(e,s){return this.server.getVariant(e,s||this._deviceId)}isFeatureEnabled(e,s,t){return this.server.isFeatureEnabled(e,s||this._deviceId,t)}track(e,s,t){this.server.track(t||this._deviceId,e,{...this.props,...s})}getResponseCookies(){let e=[];return this._newDeviceId&&e.push(`${_}=${encodeURIComponent(this._deviceId)}; Path=/; Max-Age=${S}; SameSite=Lax`),this._sessionId&&e.push(`${C}=${encodeURIComponent(this._sessionId)}; Path=/; Max-Age=${b}; SameSite=Lax`),e}getBootstrapData(e,s){let t=e||this._deviceId,r=this.server.evaluateAll(t,s),{serverConfig:n,lastEtag:i}=this.server.getConfigSnapshot();return{evalResult:r,serverConfig:n||{experiments:[],flags:[]},deviceId:this._deviceId,sessionId:this._sessionId,etag:i,generatedAt:new Date().toISOString()}}};function I(){let a=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${a}-${e}`}function x(a){let e={};if(a instanceof Headers)a.forEach((s,t)=>{e[t]=s});else for(let[s,t]of Object.entries(a))t!==void 0&&(e[s.toLowerCase()]=Array.isArray(t)?t[0]:t);return e}function w(a){let e={};if(!a)return e;for(let s of a.split(";")){let t=s.indexOf("=");if(t===-1)continue;let r=s.substring(0,t).trim(),n=s.substring(t+1).trim();try{e[r]=decodeURIComponent(n)}catch{e[r]=n}}return e}function U(a,e){if(!a)return{pathname:null,hostname:null,searchParams:null};try{let s=e?`http://${e}`:"http://localhost",t=new URL(a,s);return{pathname:t.pathname,hostname:t.hostname!=="localhost"?t.hostname:e?.split(":")[0]||null,searchParams:t.searchParams}}catch{return{pathname:a.split("?")[0]||null,hostname:null,searchParams:null}}}export{p as RequestContext,m as SplitLabServer,O as hashToFloat,A as murmurhash3};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@splitlab/node",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Node.js server SDK for SplitLab A/B testing and feature flags",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -33,7 +33,7 @@
33
33
  "dev": "tsup --watch"
34
34
  },
35
35
  "dependencies": {
36
- "@splitlab/core": "^0.3.0"
36
+ "@splitlab/core": "^0.3.2"
37
37
  },
38
38
  "devDependencies": {
39
39
  "tsup": "^8.0.0",