@variantlabs/js
v0.2.0-alpha.2
Published
VariantLabs browser SDK
Readme
@variantlabs/js
Browser SDK for VariantLabs — feature flags, experiments, and AI config, with deterministic assignment and built-in telemetry.
Using React? @variantlabs/react wraps this package in a provider and hooks.
Install
npm install @variantlabs/jsQuickstart
import { initVariantLabs } from "@variantlabs/js"
const vl = initVariantLabs({
apiKey: "pk_live_...",
appKey: "web",
environmentKey: "production",
})
await vl.init()
const { value, variantKey } = await vl.get("checkout-button-color")
document.querySelector("#buy")!.style.background = value as string
// Later — report what happened.
vl.trackOutcome({ assignmentId: ..., outcomeKey: "purchase", success: true })init() resolves once configs are available. It hydrates synchronously from localStorage first, so a returning visitor gets their assignment without waiting on the network.
Identity
You don't have to supply a subject key. On first visit the SDK generates and persists an anonymous ID in localStorage, plus a per-tab session ID in sessionStorage. Both are attached to every evaluation automatically.
// Anonymous — uses the generated persistent ID.
await vl.get("homepage-hero")
// Identified — pass your own key once the user logs in.
await vl.get("homepage-hero", { subjectKey: user.id })Because assignment is a pure hash of the subject key, the same user gets the same variant on every device and every page load. Set persistAnonymousId: false to opt out of storage entirely.
Measuring outcomes
withAssignment is the ergonomic path — it evaluates, runs your code, and records duration plus success/failure in one step.
const summary = await vl.withAssignment(
"summarizer-model",
{ subjectKey: user.id },
async (assignment) => callModel(assignment.value),
{ outcomeKey: "summary_generated" },
)Or track manually when the outcome arrives later:
const assignment = await vl.get("checkout-flow", { subjectKey: user.id })
// ...user completes checkout minutes later...
vl.trackOutcome({
assignmentId: assignment.assignmentId,
outcomeKey: "purchase",
success: true,
numericValue: order.total,
})Options
initVariantLabs({
apiKey: "pk_live_...", // required
appKey: "web", // required
environmentKey: "production", // required
baseUrl: "https://api.variantlabs.io",
persistAnonymousId: true, // localStorage anon ID (default: true)
configCacheTtlMs: 60_000, // how long a cached config stays fresh
defaultAttributes: { tier: "pro" }, // merged into every evaluation
serviceName: "storefront",
serviceVersion: "2.1.0",
emitter: { maxBatchSize: 50, flushIntervalMs: 5_000 },
logger: myLogger,
fetchImpl: myFetch, // injectable for tests
})Flushing
Events are batched and flushed on an interval. The SDK also flushes automatically when the page is hidden (visibilitychange) and on beforeunload, so you rarely need to intervene.
await vl.flush() // force a flush now
await vl.shutdown() // final flush, then stopDelivery is lossy by design — a full queue drops events and a permanently failing batch is discarded. Telemetry never blocks or crashes your page.
OpenFeature
Ships a synchronous OpenFeature web provider.
import { OpenFeature } from "@openfeature/web-sdk"
import { initVariantLabs } from "@variantlabs/js"
import { VariantLabsWebProvider } from "@variantlabs/js/openfeature"
const vl = initVariantLabs({ apiKey, appKey: "web", environmentKey: "production" })
await OpenFeature.setProviderAndWait(new VariantLabsWebProvider(vl))
const client = OpenFeature.getClient()
const enabled = client.getBooleanValue("new-checkout", false) // synchronous@openfeature/web-sdk is an optional peer dependency — install it only if you use the provider.
Caching and first render
Config is cached in localStorage under vl:config_cache:v1:{appKey}:{environmentKey} and read synchronously at startup. That means the first render already has the right variant, with no flash of the default.
A fresh cache short-circuits the network entirely, so init() can resolve without a request.
API
| | |
| --- | --- |
| initVariantLabs(options) | Create a client |
| client.init() | Load config; resolves when ready |
| client.get(key, ctx?) | Evaluate → Promise<AssignmentResult> |
| client.withAssignment(key, ctx, fn, opts?) | Evaluate, run, auto-track the outcome |
| client.trackOutcome(input) | Record an outcome |
| client.flush(opts?) / client.shutdown(opts?) | Delivery control |
| client.getContext() | Resolved SDK context |
Core types (AssignmentResult, EvaluationContext, TrackOutcomeInput, …) plus fromHeaders and sanitizeAttributes are re-exported, so you only ever import from this package.
Compatibility
Node >= 22 for tooling; any browser with global fetch at runtime. Ships ESM + CJS + type declarations. All storage access is SSR-safe — importing this package in a server render won't touch window.
License
Apache-2.0
