@splitlab/node 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 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 E=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var S=Object.prototype.hasOwnProperty;var b=(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 k(e))!S.call(n,r)&&r!==s&&m(n,r,{get:()=>e[r],enumerable:!(t=E(e,r))||t.enumerable});return n};var w=n=>x(m({},"__esModule",{value:!0}),n);var O={};b(O,{RequestContext:()=>p,SplitLabServer:()=>_,hashToFloat:()=>d.hashToFloat,murmurhash3:()=>d.murmurhash3});module.exports=w(O);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 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)}remapCustomDimensions(e){if(!e||!this.serverConfig?.custom_dimensions?.length)return e;let s={...e};for(let t of this.serverConfig.custom_dimensions)t.property_key in s&&(s[`dim_${t.index}`]=s[t.property_key],delete s[t.property_key]);return s}track(e,s,t){this.eventQueue.push({distinct_id:e,event_name:s,properties:this.remapCustomDimensions(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",C="_sl_sid",U=63072e3,T=1800,$=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],p=class{constructor(e,s){this.server=e;let t=P(s.headers),r=s.cookies||A(t.cookie||""),i=(0,l.parseUserAgent)(t["user-agent"]||void 0),a=D(s.url,t.host),u=r[y]||null;this._deviceId=u||I(),this._newDeviceId=!u;let h=r[C]||null;this._sessionId=h||I(),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 $){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){let r={...this.props,...s};this.server.track(t||this._deviceId,e,r)}getResponseCookies(){let e=[];return this._newDeviceId&&e.push(`${y}=${encodeURIComponent(this._deviceId)}; Path=/; Max-Age=${U}; SameSite=Lax`),this._sessionId&&e.push(`${C}=${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 I(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}function P(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 D(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, CustomDimensionMapping, 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.).
@@ -49,6 +53,8 @@ declare class SplitLabServer {
49
53
  * to get Set-Cookie headers for the response.
50
54
  */
51
55
  withContext(req: IncomingRequest): RequestContext;
56
+ /** Remap user property keys to dim_N slots based on custom dimension config. */
57
+ private remapCustomDimensions;
52
58
  track(distinctId: string, eventName: string, properties?: Record<string, any>): void;
53
59
  private trackExposure;
54
60
  flush(): Promise<void>;
@@ -84,6 +90,12 @@ declare class RequestContext {
84
90
  * Sets `_sl_did` (2yr) if new, always refreshes `_sl_sid` (30min sliding).
85
91
  */
86
92
  getResponseCookies(): string[];
93
+ /**
94
+ * Generate bootstrap data for client-side hydration.
95
+ * Evaluates all experiments/flags (pure, no exposure events) and packages
96
+ * the result with device ID, session ID, server config, and ETag.
97
+ */
98
+ getBootstrapData(distinctId?: string, attributes?: Record<string, any>): BootstrapData;
87
99
  }
88
100
 
89
101
  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, CustomDimensionMapping, 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.).
@@ -49,6 +53,8 @@ declare class SplitLabServer {
49
53
  * to get Set-Cookie headers for the response.
50
54
  */
51
55
  withContext(req: IncomingRequest): RequestContext;
56
+ /** Remap user property keys to dim_N slots based on custom dimension config. */
57
+ private remapCustomDimensions;
52
58
  track(distinctId: string, eventName: string, properties?: Record<string, any>): void;
53
59
  private trackExposure;
54
60
  flush(): Promise<void>;
@@ -84,6 +90,12 @@ declare class RequestContext {
84
90
  * Sets `_sl_did` (2yr) if new, always refreshes `_sl_sid` (30min sliding).
85
91
  */
86
92
  getResponseCookies(): string[];
93
+ /**
94
+ * Generate bootstrap data for client-side hydration.
95
+ * Evaluates all experiments/flags (pure, no exposure events) and packages
96
+ * the result with device ID, session ID, server config, and ETag.
97
+ */
98
+ getBootstrapData(distinctId?: string, attributes?: Record<string, any>): BootstrapData;
87
99
  }
88
100
 
89
101
  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 D}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 i=v(r.key+":"+s);if(i%1e4/100>=r.traffic_percentage)return null;let l=r.variants.reduce((o,I)=>o+I.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?R(this.serverConfig,e,s||{}):{experiments:{},flags:{}}}getConfigSnapshot(){return{serverConfig:this.serverConfig,lastEtag:this.lastEtag}}withContext(e){return new p(this,e)}remapCustomDimensions(e){if(!e||!this.serverConfig?.custom_dimensions?.length)return e;let s={...e};for(let t of this.serverConfig.custom_dimensions)t.property_key in s&&(s[`dim_${t.index}`]=s[t.property_key],delete s[t.property_key]);return s}track(e,s,t){this.eventQueue.push({distinct_id:e,event_name:s,properties:this.remapCustomDimensions(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",y="_sl_sid",k=63072e3,S=1800,b=["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=E(t["user-agent"]||void 0),i=U(s.url,t.host),g=r[_]||null;this._deviceId=g||C(),this._newDeviceId=!g;let l=r[y]||null;this._sessionId=l||C(),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 b){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){let r={...this.props,...s};this.server.track(t||this._deviceId,e,r)}getResponseCookies(){let e=[];return this._newDeviceId&&e.push(`${_}=${encodeURIComponent(this._deviceId)}; Path=/; Max-Age=${k}; SameSite=Lax`),this._sessionId&&e.push(`${y}=${encodeURIComponent(this._sessionId)}; Path=/; Max-Age=${S}; 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 C(){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,D 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.6.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.4.0"
37
37
  },
38
38
  "devDependencies": {
39
39
  "tsup": "^8.0.0",