@splitlab/js-client 0.1.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/README.md ADDED
@@ -0,0 +1,264 @@
1
+ # @splitlab/js-client
2
+
3
+ Lightweight JavaScript client SDK for SplitLab A/B testing and analytics. Zero dependencies, under 2KB gzipped.
4
+
5
+ ## Installation
6
+
7
+ ```html
8
+ <script src="https://splitlab.cc/sdk/splitlab.min.js"></script>
9
+ ```
10
+
11
+ Or build from source:
12
+
13
+ ```bash
14
+ cd sdk && npm run build
15
+ # Outputs ESM + CJS + types to dist/
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ ```javascript
21
+ import { SplitLabClient } from '@splitlab/js-client';
22
+
23
+ // No distinctId needed — SDK auto-generates a persistent device ID
24
+ const client = new SplitLabClient({
25
+ apiKey: 'ot_live_abc123',
26
+ baseUrl: 'https://splitlab.cc',
27
+ });
28
+ await client.initialize();
29
+
30
+ // A/B testing (< 1ms — local evaluation by default)
31
+ const variant = client.getVariant('checkout-btn');
32
+ if (variant === 'variant_a') showGreenButton();
33
+
34
+ // Feature flags
35
+ if (client.isFeatureEnabled('dark-mode')) enableDarkMode();
36
+
37
+ // Event tracking (auto-enriched with browser context, UTM, sessions)
38
+ await client.track('purchase', { value: 49.99, currency: 'USD' });
39
+
40
+ // When user logs in — links anonymous events to the user
41
+ await client.identify('user-42');
42
+
43
+ // Clean up
44
+ await client.destroy();
45
+ ```
46
+
47
+ ## Configuration
48
+
49
+ ```typescript
50
+ const client = new SplitLabClient({
51
+ // --- Required ---
52
+ apiKey: string; // Your org API key (ot_live_...)
53
+ baseUrl: string; // API server URL
54
+
55
+ // --- Identity ---
56
+ distinctId?: string; // User ID (optional — auto-generates device ID if omitted)
57
+
58
+ // --- Evaluation ---
59
+ ingestUrl?: string; // Ingest server URL (defaults to baseUrl)
60
+ enableLocalEvaluation?: boolean; // Evaluate locally via hashing (default: true)
61
+ configRefreshInterval?: number; // Config polling interval in ms (default: 30000)
62
+ realtimeUpdates?: boolean; // SSE for instant config push (default: false)
63
+ onConfigUpdate?: () => void; // Callback when config changes after refresh
64
+
65
+ // --- Event batching ---
66
+ autoFlushInterval?: number; // ms between auto-flushes (default: 30000)
67
+ autoFlushSize?: number; // events before auto-flush (default: 20)
68
+
69
+ // --- Analytics auto-capture ---
70
+ captureContext?: boolean; // Browser context enrichment (default: true in browser)
71
+ captureUtm?: boolean; // UTM parameter capture (default: true)
72
+ trackSessions?: boolean; // Session ID tracking (default: true)
73
+ sessionTimeout?: number; // Session inactivity timeout in ms (default: 1800000)
74
+ persistDeviceId?: boolean; // Persistent device ID in localStorage (default: true)
75
+ trackPageviews?: boolean; // Auto-track SPA navigations (default: false)
76
+ superProperties?: Record<string, any>; // Properties attached to every event
77
+ });
78
+ ```
79
+
80
+ ## Local Evaluation (Default)
81
+
82
+ The SDK defaults to local evaluation — experiments and flags are computed client-side using the same deterministic hashing as the server. On `initialize()`, the SDK fetches the org config once and evaluates locally. Subsequent `getVariant()` and `isFeatureEnabled()` calls return in < 1ms with zero network calls.
83
+
84
+ ```javascript
85
+ const client = new SplitLabClient({
86
+ apiKey: 'ot_live_abc123',
87
+ baseUrl: 'https://splitlab.cc',
88
+ // enableLocalEvaluation: true — this is the default
89
+ });
90
+ await client.initialize();
91
+ ```
92
+
93
+ Config is refreshed every 30 seconds by default. Refreshes use ETag-based conditional requests — if the config hasn't changed, the server returns `304 Not Modified` (< 5ms, no body parsed).
94
+
95
+ To disable local evaluation and use server-side assignment (which respects sticky assignments in the database):
96
+
97
+ ```javascript
98
+ const client = new SplitLabClient({
99
+ // ...
100
+ enableLocalEvaluation: false,
101
+ });
102
+ ```
103
+
104
+ Trade-off: local evaluation does not respect sticky assignments stored server-side. For first-time users the results are identical. For users who were previously assigned a variant, the server may return a different (stored) result.
105
+
106
+ ## Anonymous Users & Identity
107
+
108
+ The SDK works out of the box without a user ID. On first visit, a persistent `device_id` is generated and stored in `localStorage`. This device ID is used for:
109
+
110
+ 1. **Experiment bucketing** — variants are assigned based on device ID, so the assignment is stable across the anonymous → logged-in transition (no re-randomization)
111
+ 2. **Event attribution** — events are tagged with the device ID until `identify()` is called
112
+
113
+ ```javascript
114
+ // Anonymous — SDK generates a device ID automatically
115
+ const client = new SplitLabClient({
116
+ apiKey: 'ot_live_abc123',
117
+ baseUrl: 'https://splitlab.cc',
118
+ });
119
+ await client.initialize();
120
+
121
+ // User sees variant B (bucketed on device ID)
122
+ const variant = client.getVariant('checkout-btn'); // → 'variant_b'
123
+
124
+ // Later, user logs in
125
+ await client.identify('user-42');
126
+
127
+ // Still variant B — bucketing is device-level, not user-level
128
+ client.getVariant('checkout-btn'); // → 'variant_b' (same)
129
+
130
+ // Events now attributed to 'user-42' instead of the device ID
131
+ await client.track('purchase', { value: 49.99 });
132
+ ```
133
+
134
+ When `identify()` is called, the SDK:
135
+ - Fires a `$identify` event linking `device_id` → `user_id` (with `previous_distinct_id`)
136
+ - Switches event attribution to the new user ID
137
+ - Does **not** re-evaluate experiments (bucketing stays on device ID)
138
+
139
+ You can also provide `distinctId` upfront if the user is already authenticated:
140
+
141
+ ```javascript
142
+ const client = new SplitLabClient({
143
+ apiKey: 'ot_live_abc123',
144
+ baseUrl: 'https://splitlab.cc',
145
+ distinctId: currentUser.id, // Already logged in
146
+ });
147
+ ```
148
+
149
+ ## Realtime Config Updates
150
+
151
+ For instant config propagation (< 1 second after a dashboard edit), enable SSE:
152
+
153
+ ```javascript
154
+ const client = new SplitLabClient({
155
+ // ...
156
+ realtimeUpdates: true,
157
+ onConfigUpdate: () => {
158
+ console.log('Config updated — flags and experiments may have changed');
159
+ },
160
+ });
161
+ ```
162
+
163
+ The SDK opens an EventSource connection to the API's SSE endpoint. When an experiment or flag is created, updated, or deleted, the server pushes a `config_updated` event and the SDK refreshes automatically. EventSource handles reconnection natively.
164
+
165
+ ## Analytics Auto-Capture
166
+
167
+ Every `track()` call is automatically enriched with contextual properties (in browser environments):
168
+
169
+ **Browser context** (`captureContext: true`):
170
+ - `pathname`, `hostname`, `referrer`, `search_params`, `page_title`, `hash`
171
+ - `browser`, `browser_version`, `os`, `os_version`, `device_type` (parsed from User-Agent)
172
+ - `screen_width`, `screen_height`, `viewport_width`, `viewport_height`
173
+ - `language`, `timezone`
174
+
175
+ **UTM parameters** (`captureUtm: true`):
176
+ - `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`
177
+ - Captured on first page load and persisted in `sessionStorage` across SPA navigations
178
+
179
+ **Session tracking** (`trackSessions: true`):
180
+ - `session_id` — random ID that resets after 30 minutes of inactivity (configurable via `sessionTimeout`)
181
+
182
+ **Device ID** (`persistDeviceId: true`):
183
+ - `device_id` — persistent random ID stored in `localStorage`, survives across sessions
184
+
185
+ **Auto-pageview tracking** (`trackPageviews: true`):
186
+ - Wraps `history.pushState`, `history.replaceState`, and the `popstate` event
187
+ - Fires a `$pageview` event on each SPA navigation with `previous_url`
188
+
189
+ **Super properties** (`superProperties`):
190
+ - Key-value pairs attached to every event, overriding auto-captured values
191
+ - Modify at runtime with `setSuperProperties()` and `unsetSuperProperty()`
192
+
193
+ Property merge order (last wins): browser context < UTM < session < device < super properties < user properties.
194
+
195
+ ## Resilient Initialization
196
+
197
+ If the config fetch fails during `initialize()` and no cached config exists, the SDK falls back to safe defaults instead of throwing:
198
+ - All experiments return `null` (control)
199
+ - All flags return `false` (off)
200
+ - A warning is logged
201
+
202
+ If `refresh()` fails, the existing config is kept.
203
+
204
+ ## Direct Ingest URL
205
+
206
+ In development or when the ingest service runs on a separate host, use `ingestUrl`:
207
+
208
+ ```javascript
209
+ const client = new SplitLabClient({
210
+ apiKey: 'ot_live_abc123',
211
+ baseUrl: 'http://localhost:3001', // Node API
212
+ ingestUrl: 'http://localhost:3002', // Rust ingest
213
+ });
214
+ ```
215
+
216
+ In production behind a reverse proxy (Caddy, nginx), both services share one origin and `ingestUrl` is not needed.
217
+
218
+ ## API
219
+
220
+ ### `client.initialize(): Promise<void>`
221
+ Fetches config (or evaluation data), starts the auto-flush timer and config polling timer. If `realtimeUpdates` is enabled, opens the SSE connection.
222
+
223
+ ### `client.getVariant(experimentKey): string | null`
224
+ Returns the assigned variant key, or `null` if excluded. < 1ms with local evaluation.
225
+
226
+ ### `client.isFeatureEnabled(flagKey): boolean`
227
+ Returns whether the feature flag is enabled for this user. < 1ms with local evaluation.
228
+
229
+ ### `client.track(eventName, properties?): Promise<void>`
230
+ Queues an event (auto-enriched with context). Auto-flushes when queue reaches `autoFlushSize`.
231
+
232
+ ### `client.trackPageview(properties?): Promise<void>`
233
+ Shorthand for `track('$pageview', properties)`.
234
+
235
+ ### `client.flush(): Promise<void>`
236
+ Sends all queued events to the ingest service.
237
+
238
+ ### `client.identify(distinctId, properties?): Promise<void>`
239
+ Links the anonymous device to a user ID. Fires a `$identify` event with `previous_distinct_id`. Experiment assignments are unchanged (bucketed on device ID). Future events are attributed to the new user ID.
240
+
241
+ ### `client.getDistinctId(): string`
242
+ Returns the current distinct ID (user ID if identified, device ID otherwise).
243
+
244
+ ### `client.getDeviceId(): string`
245
+ Returns the stable device ID used for experiment bucketing.
246
+
247
+ ### `client.refresh(): Promise<void>`
248
+ Re-fetches config from the server. Uses ETag — returns immediately on 304.
249
+
250
+ ### `client.setAttributes(attributes): void`
251
+ Merges attributes for targeting rule evaluation.
252
+
253
+ ### `client.setSuperProperties(props): void`
254
+ Sets properties attached to every subsequent event.
255
+
256
+ ### `client.unsetSuperProperty(key): void`
257
+ Removes a super property by key.
258
+
259
+ ### `client.destroy(): Promise<void>`
260
+ Flushes remaining events, stops all timers, closes SSE connection.
261
+
262
+ ## React
263
+
264
+ For React bindings (provider + hooks), see [`@splitlab/react`](../react-sdk/).
package/dist/index.cjs ADDED
@@ -0,0 +1 @@
1
+ "use strict";var p=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var D=(n,e)=>{for(var t in e)p(n,t,{get:e[t],enumerable:!0})},z=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of A(e))!L.call(n,s)&&s!==t&&p(n,s,{get:()=>e[s],enumerable:!(i=O(e,s))||i.enumerable});return n};var M=n=>z(p({},"__esModule",{value:!0}),n);var $={};D($,{SplitLabClient:()=>y,hashToFloat:()=>w,murmurhash3:()=>g});module.exports=M($);function g(n,e=0){let t=e>>>0,i=n.length,s=i>>2,a=3432918353,r=461845907;for(let u=0;u<s;u++){let c=n.charCodeAt(u*4)&255|(n.charCodeAt(u*4+1)&255)<<8|(n.charCodeAt(u*4+2)&255)<<16|(n.charCodeAt(u*4+3)&255)<<24;c=Math.imul(c,a),c=c<<15|c>>>17,c=Math.imul(c,r),t^=c,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196}let o=s*4,l=0;switch(i&3){case 3:l^=(n.charCodeAt(o+2)&255)<<16;case 2:l^=(n.charCodeAt(o+1)&255)<<8;case 1:l^=n.charCodeAt(o)&255,l=Math.imul(l,a),l=l<<15|l>>>17,l=Math.imul(l,r),t^=l}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 w(n,e){return g(n,e)/4294967295}var h=typeof globalThis<"u"&&typeof globalThis.document<"u",B=[[/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"]],F=[[/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,s=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,l]of B){let u=n.match(o);if(u){e=l,t=u[1]||null;break}}for(let[o,l]of F){let u=n.match(o);if(u){i=l,s=u[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:s,device_type:r}}function T(){if(!h)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,s=n.screen,a=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:a.browser,browser_version:a.browser_version,os:a.os,os_version:a.os_version,device_type:a.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:N()}}function N(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],C="__ot_utm";function x(){if(!h)return{};try{let i=globalThis.sessionStorage?.getItem(C);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(C,JSON.stringify(e))}catch{}return e}var _="__ot_sid",m="__ot_sts";function E(n){if(!h)return f();let e=globalThis.sessionStorage;if(!e)return f();try{let t=Date.now(),i=e.getItem(_),s=Number(e.getItem(m)||"0");if(i&&t-s<n)return e.setItem(m,String(t)),i;let a=f();return e.setItem(_,a),e.setItem(m,String(t)),a}catch{return f()}}var R="__ot_did";function P(){if(!h)return f();try{let n=globalThis.localStorage;if(!n)return f();let e=n.getItem(R);if(e)return e;let t=f();return n.setItem(R,t),t}catch{return f()}}var I="__ot_ref";function U(){if(!h)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(I);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(I,t),t||null}catch{return globalThis.document?.referrer||null}}function f(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var b=typeof globalThis<"u"&&typeof globalThis.document<"u",y=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.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??b,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{}},this.config.captureUtm&&(this.utmParams=x()),this.config.captureContext&&(this.initialReferrer=U()),this.deviceId=P()}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=this.localEvaluate(this.serverConfig,this.bucketingId)}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(),b&&(this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&b&&this.setupPageviewTracking(),this.initialized=!0}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.experiments[e]??null}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=this.localEvaluate(this.serverConfig,this.bucketingId),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=T();if(i){for(let[s,a]of Object.entries(i))a!==null&&a!==""&&a!==void 0&&(t[s]=a);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=(...o)=>{s(...o),this.onUrlChange(i),i=e.location?.href};let a=t.replaceState.bind(t);t.replaceState=(...o)=>{a(...o),this.onUrlChange(i),i=e.location?.href};let r=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",r),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=s,t.replaceState=a,e.removeEventListener("popstate",r)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}localEvaluate(e,t){let i={},s={},a=this.config.attributes;for(let r of e.experiments){if(r.targeting_rules&&!this.evaluateRules(r.targeting_rules,a)){i[r.key]=null;continue}let o=g(r.key+":"+t);if(o%1e4/100>=r.traffic_percentage){i[r.key]=null;continue}let u=r.variants.reduce((v,k)=>v+k.weight,0),c=o%u,S=0,d=null;for(let v of r.variants)if(S+=v.weight,c<S){d=v.key;break}d||(d=r.variants[r.variants.length-1].key),i[r.key]=d}for(let r of e.flags){if(r.rules&&!this.evaluateRules(r.rules,a)){s[r.key]=!1;continue}let l=g(r.key+":"+t)%100;s[r.key]=l<r.rollout_percentage}return{experiments:i,flags:s}}evaluateRules(e,t){if(!e.groups||e.groups.length===0)return!0;let i=a=>{let r=t[a.attribute];if(r==null)return!1;switch(a.operator){case"is":return String(r)===String(a.value);case"is_not":return String(r)!==String(a.value);case"contains":return String(r).toLowerCase().includes(String(a.value).toLowerCase());case"not_contains":return!String(r).toLowerCase().includes(String(a.value).toLowerCase());case"gt":return Number(r)>Number(a.value);case"lt":return Number(r)<Number(a.value);case"gte":return Number(r)>=Number(a.value);case"lte":return Number(r)<=Number(a.value);case"in":return Array.isArray(a.value)&&a.value.map(String).includes(String(r));case"not_in":return Array.isArray(a.value)&&!a.value.map(String).includes(String(r));default:return!1}},s=a=>a.conditions.every(i);return e.match==="any"?e.groups.some(s):e.groups.every(s)}async fetchConfig(){let e={"Content-Type":"application/json","X-API-Key":this.config.apiKey};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=await fetch(`${this.config.baseUrl}/api/sdk/config`,{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 i=t.headers.get("etag");return{config:await t.json(),etag:i}}connectConfigStream(){if(typeof EventSource>"u")return;let e=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}`;this.eventSource=new EventSource(e),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 a=s?this.config.ingestUrl:this.config.baseUrl,r=await fetch(`${a}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!r.ok){let o=await r.text().catch(()=>"");throw new Error(`SplitLab API error ${r.status}: ${o}`)}return r.json()}};0&&(module.exports={SplitLabClient,hashToFloat,murmurhash3});
@@ -0,0 +1,160 @@
1
+ /**
2
+ * MurmurHash3_x86_32 — pure TypeScript implementation.
3
+ * Identical to api/src/utils/hash.ts — the SDK and server
4
+ * MUST produce the same output for local evaluation to work.
5
+ */
6
+ declare function murmurhash3(key: string, seed?: number): number;
7
+ /**
8
+ * Hash a key to a float in [0, 1).
9
+ */
10
+ declare function hashToFloat(key: string, seed?: number): number;
11
+
12
+ interface SplitLabConfig {
13
+ apiKey: string;
14
+ baseUrl: string;
15
+ ingestUrl?: string;
16
+ /** Unique user identifier. If omitted, the SDK auto-generates a persistent device ID. */
17
+ distinctId?: string;
18
+ attributes?: Record<string, any>;
19
+ autoFlushInterval?: number;
20
+ autoFlushSize?: number;
21
+ /** Enable local evaluation of experiments/flags (no /evaluate network call). Default: true. */
22
+ enableLocalEvaluation?: boolean;
23
+ /** Config polling interval in ms. Default: 30000 (30s). */
24
+ configRefreshInterval?: number;
25
+ /** Enable SSE for instant config push from server. Default: false. */
26
+ realtimeUpdates?: boolean;
27
+ /** Callback when config changes (after refresh). */
28
+ onConfigUpdate?: () => void;
29
+ /** Automatically enrich events with browser context (pathname, hostname, referrer, UA, screen, etc.). Default: true in browser, false in Node. */
30
+ captureContext?: boolean;
31
+ /** Automatically parse and persist UTM parameters from the URL. Default: true. */
32
+ captureUtm?: boolean;
33
+ /** Automatically track session IDs with inactivity timeout. Default: true. */
34
+ trackSessions?: boolean;
35
+ /** Session inactivity timeout in ms. Default: 1800000 (30 minutes). */
36
+ sessionTimeout?: number;
37
+ /** Persist a device ID in localStorage across sessions. Default: true. */
38
+ persistDeviceId?: boolean;
39
+ /** Automatically track pageviews on SPA navigation (History API). Default: false. */
40
+ trackPageviews?: boolean;
41
+ /** Properties attached to every tracked event. */
42
+ superProperties?: Record<string, any>;
43
+ }
44
+ interface Variant {
45
+ key: string;
46
+ weight: number;
47
+ }
48
+ interface TargetingCondition {
49
+ attribute: string;
50
+ operator: 'is' | 'is_not' | 'contains' | 'not_contains' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | 'not_in';
51
+ value: string | number | string[];
52
+ }
53
+ interface TargetingGroup {
54
+ conditions: TargetingCondition[];
55
+ }
56
+ interface TargetingRules {
57
+ match: 'all' | 'any';
58
+ groups: TargetingGroup[];
59
+ }
60
+ interface ExperimentConfig {
61
+ key: string;
62
+ traffic_percentage: number;
63
+ variants: Variant[];
64
+ targeting_rules?: TargetingRules | null;
65
+ }
66
+ interface FlagConfig {
67
+ key: string;
68
+ enabled: boolean;
69
+ rollout_percentage: number;
70
+ rules?: TargetingRules | null;
71
+ }
72
+ interface ServerConfig {
73
+ experiments: ExperimentConfig[];
74
+ flags: FlagConfig[];
75
+ }
76
+ interface EvalResult {
77
+ experiments: Record<string, string | null>;
78
+ flags: Record<string, boolean>;
79
+ }
80
+ interface TrackEvent {
81
+ distinct_id: string;
82
+ event_name: string;
83
+ properties?: Record<string, any>;
84
+ timestamp?: string;
85
+ }
86
+
87
+ interface BrowserContext {
88
+ pathname: string | null;
89
+ hostname: string | null;
90
+ referrer: string | null;
91
+ search_params: string | null;
92
+ page_title: string | null;
93
+ hash: string | null;
94
+ user_agent: string | null;
95
+ browser: string | null;
96
+ browser_version: string | null;
97
+ os: string | null;
98
+ os_version: string | null;
99
+ device_type: string | null;
100
+ screen_width: number | null;
101
+ screen_height: number | null;
102
+ viewport_width: number | null;
103
+ viewport_height: number | null;
104
+ language: string | null;
105
+ timezone: string | null;
106
+ }
107
+ declare const UTM_KEYS: readonly ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
108
+ type UtmParams = Partial<Record<(typeof UTM_KEYS)[number], string>>;
109
+
110
+ declare class SplitLabClient {
111
+ private config;
112
+ private evalResult;
113
+ private serverConfig;
114
+ private eventQueue;
115
+ private flushTimer;
116
+ private configRefreshTimer;
117
+ private initialized;
118
+ private beforeUnloadHandler;
119
+ private lastEtag;
120
+ private eventSource;
121
+ private deviceId;
122
+ private utmParams;
123
+ private initialReferrer;
124
+ private pageviewCleanup;
125
+ constructor(config: SplitLabConfig);
126
+ /** The effective distinct ID used for event attribution. */
127
+ private get effectiveDistinctId();
128
+ /** The stable ID used for experiment bucketing (always device-level). */
129
+ private get bucketingId();
130
+ initialize(): Promise<void>;
131
+ getVariant(experimentKey: string): string | null;
132
+ isFeatureEnabled(flagKey: string): boolean;
133
+ track(eventName: string, properties?: Record<string, any>): Promise<void>;
134
+ /** Track a pageview event with the current URL context. */
135
+ trackPageview(properties?: Record<string, any>): Promise<void>;
136
+ /** Set properties that will be attached to every subsequent event. */
137
+ setSuperProperties(props: Record<string, any>): void;
138
+ /** Remove a super property by key. */
139
+ unsetSuperProperty(key: string): void;
140
+ flush(): Promise<void>;
141
+ identify(distinctId: string, properties?: Record<string, any>): Promise<void>;
142
+ refresh(): Promise<void>;
143
+ /** Returns the current distinct ID (user ID if identified, device ID otherwise). */
144
+ getDistinctId(): string;
145
+ /** Returns the stable device ID used for experiment bucketing. */
146
+ getDeviceId(): string;
147
+ setAttributes(attributes: Record<string, any>): void;
148
+ destroy(): Promise<void>;
149
+ private enrichProperties;
150
+ private setupPageviewTracking;
151
+ private onUrlChange;
152
+ private localEvaluate;
153
+ private evaluateRules;
154
+ private fetchConfig;
155
+ private connectConfigStream;
156
+ private flushSync;
157
+ private request;
158
+ }
159
+
160
+ export { type BrowserContext, type EvalResult, type ExperimentConfig, type FlagConfig, type ServerConfig, SplitLabClient, type SplitLabConfig, type TargetingCondition, type TargetingGroup, type TargetingRules, type TrackEvent, type UtmParams, type Variant, hashToFloat, murmurhash3 };
@@ -0,0 +1,160 @@
1
+ /**
2
+ * MurmurHash3_x86_32 — pure TypeScript implementation.
3
+ * Identical to api/src/utils/hash.ts — the SDK and server
4
+ * MUST produce the same output for local evaluation to work.
5
+ */
6
+ declare function murmurhash3(key: string, seed?: number): number;
7
+ /**
8
+ * Hash a key to a float in [0, 1).
9
+ */
10
+ declare function hashToFloat(key: string, seed?: number): number;
11
+
12
+ interface SplitLabConfig {
13
+ apiKey: string;
14
+ baseUrl: string;
15
+ ingestUrl?: string;
16
+ /** Unique user identifier. If omitted, the SDK auto-generates a persistent device ID. */
17
+ distinctId?: string;
18
+ attributes?: Record<string, any>;
19
+ autoFlushInterval?: number;
20
+ autoFlushSize?: number;
21
+ /** Enable local evaluation of experiments/flags (no /evaluate network call). Default: true. */
22
+ enableLocalEvaluation?: boolean;
23
+ /** Config polling interval in ms. Default: 30000 (30s). */
24
+ configRefreshInterval?: number;
25
+ /** Enable SSE for instant config push from server. Default: false. */
26
+ realtimeUpdates?: boolean;
27
+ /** Callback when config changes (after refresh). */
28
+ onConfigUpdate?: () => void;
29
+ /** Automatically enrich events with browser context (pathname, hostname, referrer, UA, screen, etc.). Default: true in browser, false in Node. */
30
+ captureContext?: boolean;
31
+ /** Automatically parse and persist UTM parameters from the URL. Default: true. */
32
+ captureUtm?: boolean;
33
+ /** Automatically track session IDs with inactivity timeout. Default: true. */
34
+ trackSessions?: boolean;
35
+ /** Session inactivity timeout in ms. Default: 1800000 (30 minutes). */
36
+ sessionTimeout?: number;
37
+ /** Persist a device ID in localStorage across sessions. Default: true. */
38
+ persistDeviceId?: boolean;
39
+ /** Automatically track pageviews on SPA navigation (History API). Default: false. */
40
+ trackPageviews?: boolean;
41
+ /** Properties attached to every tracked event. */
42
+ superProperties?: Record<string, any>;
43
+ }
44
+ interface Variant {
45
+ key: string;
46
+ weight: number;
47
+ }
48
+ interface TargetingCondition {
49
+ attribute: string;
50
+ operator: 'is' | 'is_not' | 'contains' | 'not_contains' | 'gt' | 'lt' | 'gte' | 'lte' | 'in' | 'not_in';
51
+ value: string | number | string[];
52
+ }
53
+ interface TargetingGroup {
54
+ conditions: TargetingCondition[];
55
+ }
56
+ interface TargetingRules {
57
+ match: 'all' | 'any';
58
+ groups: TargetingGroup[];
59
+ }
60
+ interface ExperimentConfig {
61
+ key: string;
62
+ traffic_percentage: number;
63
+ variants: Variant[];
64
+ targeting_rules?: TargetingRules | null;
65
+ }
66
+ interface FlagConfig {
67
+ key: string;
68
+ enabled: boolean;
69
+ rollout_percentage: number;
70
+ rules?: TargetingRules | null;
71
+ }
72
+ interface ServerConfig {
73
+ experiments: ExperimentConfig[];
74
+ flags: FlagConfig[];
75
+ }
76
+ interface EvalResult {
77
+ experiments: Record<string, string | null>;
78
+ flags: Record<string, boolean>;
79
+ }
80
+ interface TrackEvent {
81
+ distinct_id: string;
82
+ event_name: string;
83
+ properties?: Record<string, any>;
84
+ timestamp?: string;
85
+ }
86
+
87
+ interface BrowserContext {
88
+ pathname: string | null;
89
+ hostname: string | null;
90
+ referrer: string | null;
91
+ search_params: string | null;
92
+ page_title: string | null;
93
+ hash: string | null;
94
+ user_agent: string | null;
95
+ browser: string | null;
96
+ browser_version: string | null;
97
+ os: string | null;
98
+ os_version: string | null;
99
+ device_type: string | null;
100
+ screen_width: number | null;
101
+ screen_height: number | null;
102
+ viewport_width: number | null;
103
+ viewport_height: number | null;
104
+ language: string | null;
105
+ timezone: string | null;
106
+ }
107
+ declare const UTM_KEYS: readonly ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
108
+ type UtmParams = Partial<Record<(typeof UTM_KEYS)[number], string>>;
109
+
110
+ declare class SplitLabClient {
111
+ private config;
112
+ private evalResult;
113
+ private serverConfig;
114
+ private eventQueue;
115
+ private flushTimer;
116
+ private configRefreshTimer;
117
+ private initialized;
118
+ private beforeUnloadHandler;
119
+ private lastEtag;
120
+ private eventSource;
121
+ private deviceId;
122
+ private utmParams;
123
+ private initialReferrer;
124
+ private pageviewCleanup;
125
+ constructor(config: SplitLabConfig);
126
+ /** The effective distinct ID used for event attribution. */
127
+ private get effectiveDistinctId();
128
+ /** The stable ID used for experiment bucketing (always device-level). */
129
+ private get bucketingId();
130
+ initialize(): Promise<void>;
131
+ getVariant(experimentKey: string): string | null;
132
+ isFeatureEnabled(flagKey: string): boolean;
133
+ track(eventName: string, properties?: Record<string, any>): Promise<void>;
134
+ /** Track a pageview event with the current URL context. */
135
+ trackPageview(properties?: Record<string, any>): Promise<void>;
136
+ /** Set properties that will be attached to every subsequent event. */
137
+ setSuperProperties(props: Record<string, any>): void;
138
+ /** Remove a super property by key. */
139
+ unsetSuperProperty(key: string): void;
140
+ flush(): Promise<void>;
141
+ identify(distinctId: string, properties?: Record<string, any>): Promise<void>;
142
+ refresh(): Promise<void>;
143
+ /** Returns the current distinct ID (user ID if identified, device ID otherwise). */
144
+ getDistinctId(): string;
145
+ /** Returns the stable device ID used for experiment bucketing. */
146
+ getDeviceId(): string;
147
+ setAttributes(attributes: Record<string, any>): void;
148
+ destroy(): Promise<void>;
149
+ private enrichProperties;
150
+ private setupPageviewTracking;
151
+ private onUrlChange;
152
+ private localEvaluate;
153
+ private evaluateRules;
154
+ private fetchConfig;
155
+ private connectConfigStream;
156
+ private flushSync;
157
+ private request;
158
+ }
159
+
160
+ export { type BrowserContext, type EvalResult, type ExperimentConfig, type FlagConfig, type ServerConfig, SplitLabClient, type SplitLabConfig, type TargetingCondition, type TargetingGroup, type TargetingRules, type TrackEvent, type UtmParams, type Variant, hashToFloat, murmurhash3 };
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ function g(s,e=0){let t=e>>>0,i=s.length,a=i>>2,r=3432918353,n=461845907;for(let u=0;u<a;u++){let c=s.charCodeAt(u*4)&255|(s.charCodeAt(u*4+1)&255)<<8|(s.charCodeAt(u*4+2)&255)<<16|(s.charCodeAt(u*4+3)&255)<<24;c=Math.imul(c,r),c=c<<15|c>>>17,c=Math.imul(c,n),t^=c,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196}let o=a*4,l=0;switch(i&3){case 3:l^=(s.charCodeAt(o+2)&255)<<16;case 2:l^=(s.charCodeAt(o+1)&255)<<8;case 1:l^=s.charCodeAt(o)&255,l=Math.imul(l,r),l=l<<15|l>>>17,l=Math.imul(l,n),t^=l}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 U(s,e){return g(s,e)/4294967295}var h=typeof globalThis<"u"&&typeof globalThis.document<"u",k=[[/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"]],O=[[/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 A(s){if(!s)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(s)&&!/Chrome|CriOS|Chromium|Edg|OPR|SamsungBrowser|UCBrowser/.test(s)){let o=s.match(/Version\/(\S+)/);e="Safari",t=o?o[1]:null}else for(let[o,l]of k){let u=s.match(o);if(u){e=l,t=u[1]||null;break}}for(let[o,l]of O){let u=s.match(o);if(u){i=l,a=u[1]?.replace(/_/g,".")||null;break}}let n="desktop";return/Mobi|Android.*Mobile|iPhone/.test(s)?n="mobile":/iPad|Android(?!.*Mobile)|Tablet/.test(s)&&(n="tablet"),{browser:e,browser_version:t,os:i,os_version:a,device_type:n}}function _(){if(!h)return null;let s=globalThis,e=s.navigator,t=s.location,i=s.document,a=s.screen,r=A(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:s.innerWidth??null,viewport_height:s.innerHeight??null,language:e?.language??null,timezone:L()}}function L(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var D=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],y="__ot_utm";function R(){if(!h)return{};try{let i=globalThis.sessionStorage?.getItem(y);if(i)return JSON.parse(i)}catch{}let s=new URLSearchParams(globalThis.location?.search||""),e={},t=!1;for(let i of D){let a=s.get(i);a&&(e[i]=a,t=!0)}if(t)try{globalThis.sessionStorage?.setItem(y,JSON.stringify(e))}catch{}return e}var S="__ot_sid",p="__ot_sts";function I(s){if(!h)return f();let e=globalThis.sessionStorage;if(!e)return f();try{let t=Date.now(),i=e.getItem(S),a=Number(e.getItem(p)||"0");if(i&&t-a<s)return e.setItem(p,String(t)),i;let r=f();return e.setItem(S,r),e.setItem(p,String(t)),r}catch{return f()}}var w="__ot_did";function T(){if(!h)return f();try{let s=globalThis.localStorage;if(!s)return f();let e=s.getItem(w);if(e)return e;let t=f();return s.setItem(w,t),t}catch{return f()}}var C="__ot_ref";function x(){if(!h)return null;try{let s=globalThis.sessionStorage,e=s?.getItem(C);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return s?.setItem(C,t),t||null}catch{return globalThis.document?.referrer||null}}function f(){let s=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${s}-${e}`}var m=typeof globalThis<"u"&&typeof globalThis.document<"u",E=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.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??m,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{}},this.config.captureUtm&&(this.utmParams=R()),this.config.captureContext&&(this.initialReferrer=x()),this.deviceId=T()}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=this.localEvaluate(this.serverConfig,this.bucketingId)}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(),m&&(this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&m&&this.setupPageviewTracking(),this.initialized=!0}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.experiments[e]??null}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=this.localEvaluate(this.serverConfig,this.bucketingId),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=_();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=I(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=(...o)=>{a(...o),this.onUrlChange(i),i=e.location?.href};let r=t.replaceState.bind(t);t.replaceState=(...o)=>{r(...o),this.onUrlChange(i),i=e.location?.href};let n=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",n),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=a,t.replaceState=r,e.removeEventListener("popstate",n)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}localEvaluate(e,t){let i={},a={},r=this.config.attributes;for(let n of e.experiments){if(n.targeting_rules&&!this.evaluateRules(n.targeting_rules,r)){i[n.key]=null;continue}let o=g(n.key+":"+t);if(o%1e4/100>=n.traffic_percentage){i[n.key]=null;continue}let u=n.variants.reduce((v,P)=>v+P.weight,0),c=o%u,b=0,d=null;for(let v of n.variants)if(b+=v.weight,c<b){d=v.key;break}d||(d=n.variants[n.variants.length-1].key),i[n.key]=d}for(let n of e.flags){if(n.rules&&!this.evaluateRules(n.rules,r)){a[n.key]=!1;continue}let l=g(n.key+":"+t)%100;a[n.key]=l<n.rollout_percentage}return{experiments:i,flags:a}}evaluateRules(e,t){if(!e.groups||e.groups.length===0)return!0;let i=r=>{let n=t[r.attribute];if(n==null)return!1;switch(r.operator){case"is":return String(n)===String(r.value);case"is_not":return String(n)!==String(r.value);case"contains":return String(n).toLowerCase().includes(String(r.value).toLowerCase());case"not_contains":return!String(n).toLowerCase().includes(String(r.value).toLowerCase());case"gt":return Number(n)>Number(r.value);case"lt":return Number(n)<Number(r.value);case"gte":return Number(n)>=Number(r.value);case"lte":return Number(n)<=Number(r.value);case"in":return Array.isArray(r.value)&&r.value.map(String).includes(String(n));case"not_in":return Array.isArray(r.value)&&!r.value.map(String).includes(String(n));default:return!1}},a=r=>r.conditions.every(i);return e.match==="any"?e.groups.some(a):e.groups.every(a)}async fetchConfig(){let e={"Content-Type":"application/json","X-API-Key":this.config.apiKey};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=await fetch(`${this.config.baseUrl}/api/sdk/config`,{method:"GET",headers:e});if(t.status===304)return{config:this.serverConfig,etag:this.lastEtag,notModified:!0};if(!t.ok){let r=await t.text().catch(()=>"");throw new Error(`SplitLab API error ${t.status}: ${r}`)}let i=t.headers.get("etag");return{config:await t.json(),etag:i}}connectConfigStream(){if(typeof EventSource>"u")return;let e=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}`;this.eventSource=new EventSource(e),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,n=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(!n.ok){let o=await n.text().catch(()=>"");throw new Error(`SplitLab API error ${n.status}: ${o}`)}return n.json()}};export{E as SplitLabClient,U as hashToFloat,g as murmurhash3};
@@ -0,0 +1 @@
1
+ "use strict";var SplitLab=(()=>{var p=Object.defineProperty;var O=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var L=Object.prototype.hasOwnProperty;var D=(n,e)=>{for(var t in e)p(n,t,{get:e[t],enumerable:!0})},z=(n,e,t,i)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of A(e))!L.call(n,s)&&s!==t&&p(n,s,{get:()=>e[s],enumerable:!(i=O(e,s))||i.enumerable});return n};var M=n=>z(p({},"__esModule",{value:!0}),n);var $={};D($,{SplitLabClient:()=>y,hashToFloat:()=>w,murmurhash3:()=>g});function g(n,e=0){let t=e>>>0,i=n.length,s=i>>2,a=3432918353,r=461845907;for(let u=0;u<s;u++){let c=n.charCodeAt(u*4)&255|(n.charCodeAt(u*4+1)&255)<<8|(n.charCodeAt(u*4+2)&255)<<16|(n.charCodeAt(u*4+3)&255)<<24;c=Math.imul(c,a),c=c<<15|c>>>17,c=Math.imul(c,r),t^=c,t=t<<13|t>>>19,t=Math.imul(t,5)+3864292196}let o=s*4,l=0;switch(i&3){case 3:l^=(n.charCodeAt(o+2)&255)<<16;case 2:l^=(n.charCodeAt(o+1)&255)<<8;case 1:l^=n.charCodeAt(o)&255,l=Math.imul(l,a),l=l<<15|l>>>17,l=Math.imul(l,r),t^=l}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 w(n,e){return g(n,e)/4294967295}var h=typeof globalThis<"u"&&typeof globalThis.document<"u",B=[[/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"]],F=[[/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,s=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,l]of B){let u=n.match(o);if(u){e=l,t=u[1]||null;break}}for(let[o,l]of F){let u=n.match(o);if(u){i=l,s=u[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:s,device_type:r}}function T(){if(!h)return null;let n=globalThis,e=n.navigator,t=n.location,i=n.document,s=n.screen,a=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:a.browser,browser_version:a.browser_version,os:a.os,os_version:a.os_version,device_type:a.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:N()}}function N(){try{return Intl.DateTimeFormat().resolvedOptions().timeZone}catch{return null}}var j=["utm_source","utm_medium","utm_campaign","utm_term","utm_content"],C="__ot_utm";function x(){if(!h)return{};try{let i=globalThis.sessionStorage?.getItem(C);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(C,JSON.stringify(e))}catch{}return e}var _="__ot_sid",m="__ot_sts";function E(n){if(!h)return f();let e=globalThis.sessionStorage;if(!e)return f();try{let t=Date.now(),i=e.getItem(_),s=Number(e.getItem(m)||"0");if(i&&t-s<n)return e.setItem(m,String(t)),i;let a=f();return e.setItem(_,a),e.setItem(m,String(t)),a}catch{return f()}}var R="__ot_did";function P(){if(!h)return f();try{let n=globalThis.localStorage;if(!n)return f();let e=n.getItem(R);if(e)return e;let t=f();return n.setItem(R,t),t}catch{return f()}}var I="__ot_ref";function U(){if(!h)return null;try{let n=globalThis.sessionStorage,e=n?.getItem(I);if(e!==null)return e||null;let t=globalThis.document?.referrer||"";return n?.setItem(I,t),t||null}catch{return globalThis.document?.referrer||null}}function f(){let n=Date.now().toString(36),e=Math.random().toString(36).substring(2,10);return`${n}-${e}`}var b=typeof globalThis<"u"&&typeof globalThis.document<"u",y=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.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??b,captureUtm:e.captureUtm??!0,trackSessions:e.trackSessions??!0,sessionTimeout:e.sessionTimeout??30*6e4,persistDeviceId:e.persistDeviceId??!0,trackPageviews:e.trackPageviews??!1,superProperties:e.superProperties??{}},this.config.captureUtm&&(this.utmParams=x()),this.config.captureContext&&(this.initialReferrer=U()),this.deviceId=P()}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=this.localEvaluate(this.serverConfig,this.bucketingId)}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(),b&&(this.beforeUnloadHandler=()=>{this.flushSync()},globalThis.addEventListener("beforeunload",this.beforeUnloadHandler)),this.config.trackPageviews&&b&&this.setupPageviewTracking(),this.initialized=!0}getVariant(e){if(!this.initialized||!this.evalResult)throw new Error("SplitLabClient not initialized. Call initialize() first.");return this.evalResult.experiments[e]??null}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=this.localEvaluate(this.serverConfig,this.bucketingId),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=T();if(i){for(let[s,a]of Object.entries(i))a!==null&&a!==""&&a!==void 0&&(t[s]=a);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=(...o)=>{s(...o),this.onUrlChange(i),i=e.location?.href};let a=t.replaceState.bind(t);t.replaceState=(...o)=>{a(...o),this.onUrlChange(i),i=e.location?.href};let r=()=>{this.onUrlChange(i),i=e.location?.href};e.addEventListener("popstate",r),this.trackPageview().catch(()=>{}),this.pageviewCleanup=()=>{t.pushState=s,t.replaceState=a,e.removeEventListener("popstate",r)}}onUrlChange(e){globalThis.location?.href!==e&&setTimeout(()=>{this.trackPageview({previous_url:e||null}).catch(()=>{})},0)}localEvaluate(e,t){let i={},s={},a=this.config.attributes;for(let r of e.experiments){if(r.targeting_rules&&!this.evaluateRules(r.targeting_rules,a)){i[r.key]=null;continue}let o=g(r.key+":"+t);if(o%1e4/100>=r.traffic_percentage){i[r.key]=null;continue}let u=r.variants.reduce((v,k)=>v+k.weight,0),c=o%u,S=0,d=null;for(let v of r.variants)if(S+=v.weight,c<S){d=v.key;break}d||(d=r.variants[r.variants.length-1].key),i[r.key]=d}for(let r of e.flags){if(r.rules&&!this.evaluateRules(r.rules,a)){s[r.key]=!1;continue}let l=g(r.key+":"+t)%100;s[r.key]=l<r.rollout_percentage}return{experiments:i,flags:s}}evaluateRules(e,t){if(!e.groups||e.groups.length===0)return!0;let i=a=>{let r=t[a.attribute];if(r==null)return!1;switch(a.operator){case"is":return String(r)===String(a.value);case"is_not":return String(r)!==String(a.value);case"contains":return String(r).toLowerCase().includes(String(a.value).toLowerCase());case"not_contains":return!String(r).toLowerCase().includes(String(a.value).toLowerCase());case"gt":return Number(r)>Number(a.value);case"lt":return Number(r)<Number(a.value);case"gte":return Number(r)>=Number(a.value);case"lte":return Number(r)<=Number(a.value);case"in":return Array.isArray(a.value)&&a.value.map(String).includes(String(r));case"not_in":return Array.isArray(a.value)&&!a.value.map(String).includes(String(r));default:return!1}},s=a=>a.conditions.every(i);return e.match==="any"?e.groups.some(s):e.groups.every(s)}async fetchConfig(){let e={"Content-Type":"application/json","X-API-Key":this.config.apiKey};this.lastEtag&&(e["If-None-Match"]=this.lastEtag);let t=await fetch(`${this.config.baseUrl}/api/sdk/config`,{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 i=t.headers.get("etag");return{config:await t.json(),etag:i}}connectConfigStream(){if(typeof EventSource>"u")return;let e=`${this.config.baseUrl}/api/sdk/config/stream?key=${encodeURIComponent(this.config.apiKey)}`;this.eventSource=new EventSource(e),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 a=s?this.config.ingestUrl:this.config.baseUrl,r=await fetch(`${a}${t}`,{method:e,headers:{"Content-Type":"application/json","X-API-Key":this.config.apiKey},body:i?JSON.stringify(i):void 0});if(!r.ok){let o=await r.text().catch(()=>"");throw new Error(`SplitLab API error ${r.status}: ${o}`)}return r.json()}};return M($);})();
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@splitlab/js-client",
3
+ "version": "0.1.0",
4
+ "description": "Lightweight JavaScript client SDK for SplitLab A/B testing and analytics",
5
+ "license": "MIT",
6
+ "publishConfig": {
7
+ "access": "public"
8
+ },
9
+ "type": "module",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.mts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.cts",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ }
24
+ },
25
+ "files": [
26
+ "dist"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsup",
30
+ "dev": "tsup --watch"
31
+ },
32
+ "devDependencies": {
33
+ "tsup": "^8.0.0",
34
+ "typescript": "^5.4.0"
35
+ }
36
+ }