@splitlab/js-client
v0.10.3
Published
Lightweight JavaScript client SDK for SplitLab A/B testing and analytics
Readme
@splitlab/js-client
Lightweight JavaScript client SDK for SplitLab A/B testing and analytics. Zero dependencies, under 2KB gzipped.
Installation
<script src="https://splitlab.cc/sdk/splitlab.min.js"></script>Or build from source:
cd sdk && npm run build
# Outputs ESM + CJS + types to dist/Quick Start
import { SplitLabClient } from '@splitlab/js-client';
// No distinctId needed — SDK auto-generates a persistent device ID
const client = new SplitLabClient({
apiKey: 'ot_live_abc123',
baseUrl: 'https://splitlab.cc',
});
await client.initialize();
// A/B testing (< 1ms — local evaluation by default)
const variant = client.getVariant('checkout-btn');
if (variant === 'variant_a') showGreenButton();
// Feature flags
if (client.isFeatureEnabled('dark-mode')) enableDarkMode();
// Event tracking (auto-enriched with browser context, UTM, sessions)
await client.track('purchase', { value: 49.99, currency: 'USD' });
// When user logs in — links anonymous events to the user
await client.identify('user-42');
// Clean up
await client.destroy();Configuration
const client = new SplitLabClient({
// --- Required ---
apiKey: string; // Your org API key (ot_live_...)
baseUrl: string; // API server URL
// --- Identity ---
distinctId?: string; // User ID (optional — auto-generates device ID if omitted)
// --- Evaluation ---
ingestUrl?: string; // Ingest server URL (defaults to baseUrl)
enableLocalEvaluation?: boolean; // Evaluate locally via hashing (default: true)
configRefreshInterval?: number; // Config polling interval in ms (default: 30000)
realtimeUpdates?: boolean; // SSE for instant config push (default: false)
onConfigUpdate?: () => void; // Callback when config changes after refresh
// --- Event batching ---
autoFlushInterval?: number; // ms between auto-flushes (default: 30000)
autoFlushSize?: number; // events before auto-flush (default: 20)
// --- Analytics auto-capture ---
captureContext?: boolean; // Browser context enrichment (default: true in browser)
captureUtm?: boolean; // UTM parameter capture (default: true)
trackSessions?: boolean; // Session ID tracking (default: true)
sessionTimeout?: number; // Session inactivity timeout in ms (default: 1800000)
persistDeviceId?: boolean; // Persistent device ID in localStorage (default: true)
trackPageviews?: boolean; // Auto-track SPA navigations (default: false)
superProperties?: Record<string, any>; // Properties attached to every event
});Local Evaluation (Default)
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.
const client = new SplitLabClient({
apiKey: 'ot_live_abc123',
baseUrl: 'https://splitlab.cc',
// enableLocalEvaluation: true — this is the default
});
await client.initialize();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).
To disable local evaluation and use server-side assignment (which respects sticky assignments in the database):
const client = new SplitLabClient({
// ...
enableLocalEvaluation: false,
});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.
Anonymous Users & Identity
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:
- Experiment bucketing — variants are assigned based on device ID, so the assignment is stable across the anonymous → logged-in transition (no re-randomization)
- Event attribution — events are tagged with the device ID until
identify()is called
// Anonymous — SDK generates a device ID automatically
const client = new SplitLabClient({
apiKey: 'ot_live_abc123',
baseUrl: 'https://splitlab.cc',
});
await client.initialize();
// User sees variant B (bucketed on device ID)
const variant = client.getVariant('checkout-btn'); // → 'variant_b'
// Later, user logs in
await client.identify('user-42');
// Still variant B — bucketing is device-level, not user-level
client.getVariant('checkout-btn'); // → 'variant_b' (same)
// Events now attributed to 'user-42' instead of the device ID
await client.track('purchase', { value: 49.99 });When identify() is called, the SDK:
- Fires a
$identifyevent linkingdevice_id→user_id(withprevious_distinct_id) - Switches event attribution to the new user ID
- Does not re-evaluate experiments (bucketing stays on device ID)
You can also provide distinctId upfront if the user is already authenticated:
const client = new SplitLabClient({
apiKey: 'ot_live_abc123',
baseUrl: 'https://splitlab.cc',
distinctId: currentUser.id, // Already logged in
});Realtime Config Updates
For instant config propagation (< 1 second after a dashboard edit), enable SSE:
const client = new SplitLabClient({
// ...
realtimeUpdates: true,
onConfigUpdate: () => {
console.log('Config updated — flags and experiments may have changed');
},
});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.
Analytics Auto-Capture
Every track() call is automatically enriched with contextual properties (in browser environments):
Browser context (captureContext: true):
pathname,hostname,referrer,search_params,page_title,hashbrowser,browser_version,os,os_version,device_type(parsed from User-Agent)screen_width,screen_height,viewport_width,viewport_heightlanguage,timezone
UTM parameters (captureUtm: true):
utm_source,utm_medium,utm_campaign,utm_term,utm_content- Captured on first page load and persisted in
sessionStorageacross SPA navigations
Session tracking (trackSessions: true):
session_id— random ID that resets after 30 minutes of inactivity (configurable viasessionTimeout)
Device ID (persistDeviceId: true):
device_id— persistent random ID stored inlocalStorage, survives across sessions
Auto-pageview tracking (trackPageviews: true):
- Wraps
history.pushState,history.replaceState, and thepopstateevent - Fires a
$pageviewevent on each SPA navigation withprevious_url
Super properties (superProperties):
- Key-value pairs attached to every event, overriding auto-captured values
- Modify at runtime with
setSuperProperties()andunsetSuperProperty()
Property merge order (last wins): browser context < UTM < session < device < super properties < user properties.
Resilient Initialization
If the config fetch fails during initialize() and no cached config exists, the SDK falls back to safe defaults instead of throwing:
- All experiments return
null(control) - All flags return
false(off) - A warning is logged
If refresh() fails, the existing config is kept.
Direct Ingest URL
In development or when the ingest service runs on a separate host, use ingestUrl:
const client = new SplitLabClient({
apiKey: 'ot_live_abc123',
baseUrl: 'http://localhost:3001', // API server
ingestUrl: 'http://localhost:3002', // Ingest server
});In production behind a reverse proxy (Caddy, nginx), both services share one origin and ingestUrl is not needed.
API
client.initialize(): Promise<void>
Fetches config (or evaluation data), starts the auto-flush timer and config polling timer. If realtimeUpdates is enabled, opens the SSE connection.
client.getVariant(experimentKey): string | null
Returns the assigned variant key, or null if excluded. < 1ms with local evaluation.
client.isFeatureEnabled(flagKey): boolean
Returns whether the feature flag is enabled for this user. < 1ms with local evaluation.
client.track(eventName, properties?): Promise<void>
Queues an event (auto-enriched with context). Auto-flushes when queue reaches autoFlushSize.
client.trackPageview(properties?): Promise<void>
Shorthand for track('$pageview', properties).
client.flush(): Promise<void>
Sends all queued events to the ingest service.
client.identify(distinctId, properties?): Promise<void>
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.
client.getDistinctId(): string
Returns the current distinct ID (user ID if identified, device ID otherwise).
client.getDeviceId(): string
Returns the stable device ID used for experiment bucketing.
client.refresh(): Promise<void>
Re-fetches config from the server. Uses ETag — returns immediately on 304.
client.setAttributes(attributes): void
Merges attributes for targeting rule evaluation.
client.setSuperProperties(props): void
Sets properties attached to every subsequent event.
client.unsetSuperProperty(key): void
Removes a super property by key.
client.destroy(): Promise<void>
Flushes remaining events, stops all timers, closes SSE connection.
React
For React bindings (provider + hooks), see @splitlab/react.
