@variantlabs/node
v0.2.0-alpha.2
Published
VariantLabs Node/server SDK
Readme
@variantlabs/node
Server SDK for VariantLabs — feature flags, experiments, and AI config for Node services, with background config polling, graceful shutdown, and first-class OpenTelemetry support.
Install
npm install @variantlabs/nodeQuickstart
import { initVariantLabs } from "@variantlabs/node"
const vl = initVariantLabs({
apiKey: process.env.VARIANTLABS_API_KEY!,
appKey: "api",
environmentKey: "production",
})
await vl.init()
app.get("/checkout", async (req, res) => {
const { value } = await vl.get("checkout-flow", { subjectKey: req.user.id })
res.render(value === "v2" ? "checkout-v2" : "checkout-v1")
})Create the client once per process and reuse it. Each instance runs its own poller and event queue.
Config polling
The client fetches config immediately on init(), then refreshes on an interval. The timer is unref'd, so it never keeps your process alive.
initVariantLabs({
...,
configRefreshMs: 60_000, // default; 0 disables polling entirely
})Unlike the browser SDK there's no persistence and no TTL short-circuit — a server always fetches on startup, so a deploy never serves stale assignments.
Graceful shutdown
SIGTERM and SIGINT handlers are attached by default; both flush pending events before exiting. Set handleTermSignals: false if you manage lifecycle yourself, then call shutdown() at the right moment.
initVariantLabs({ ..., handleTermSignals: false })
process.on("SIGTERM", async () => {
await vl.shutdown()
server.close()
})Deployment context
deploymentVersion is resolved automatically from the environment, so assignments can be attributed to a specific release without any wiring. First match wins:
VARIANT_DEPLOYMENT_VERSION → GIT_SHA → IMAGE_TAG → VERCEL_GIT_COMMIT_SHA → "unknown"
serviceName and serviceVersion fall back to VARIANT_SERVICE_NAME and VARIANT_SERVICE_VERSION. Anything passed explicitly to initVariantLabs wins over the environment.
Measuring outcomes
const answer = await vl.withAssignment(
"summarizer-model",
{ subjectKey: req.user.id, requestId: req.id },
async (assignment) => callModel(assignment.value),
{ outcomeKey: "summary_generated" },
)withAssignment records duration and success/error automatically. For AI workloads, report token usage on the outcome:
vl.trackOutcome({
assignmentId: assignment.assignmentId,
outcomeKey: "completion",
success: true,
inputTokens: usage.input_tokens,
outputTokens: usage.output_tokens,
})Request context from headers
import { fromHeaders } from "@variantlabs/node"
const ctx = fromHeaders(req.headers) // picks up trace/request correlation
await vl.get("my-flag", { ...ctx, subjectKey: req.user.id })OpenTelemetry
@opentelemetry/api is an optional peer dependency. Without it every OTel helper is a silent no-op.
import {
VariantLabsSpanProcessor,
withAssignmentContext,
getActiveAssignment,
} from "@variantlabs/node"
provider.addSpanProcessor(new VariantLabsSpanProcessor())
await withAssignmentContext(assignment, async () => {
// Every span started in here — and in downstream services, via baggage —
// carries the variant assignment.
await handleRequest()
})Attribute and event names follow the OpenTelemetry feature_flag semantic conventions; the contract is frozen and fixture-tested. See docs/otel-conventions.md.
Subject keys are hashed before reaching telemetry — raw keys never touch a span.
There's a runnable Express + OTel example at examples/express-otel.
OpenFeature
Ships an async OpenFeature server provider — the key difference from the browser provider, which resolves synchronously.
import { OpenFeature } from "@openfeature/server-sdk"
import { initVariantLabs } from "@variantlabs/node"
import { VariantLabsServerProvider } from "@variantlabs/node/openfeature"
const vl = initVariantLabs({ apiKey, appKey: "api", environmentKey: "production" })
await OpenFeature.setProviderAndWait(new VariantLabsServerProvider(vl))
const client = OpenFeature.getClient()
const enabled = await client.getBooleanValue("new-checkout", false, { targetingKey: userId })@openfeature/server-sdk is an optional peer dependency.
Options
initVariantLabs({
apiKey: process.env.VARIANTLABS_API_KEY!, // required
appKey: "api", // required
environmentKey: "production", // required
baseUrl: "https://api.variantlabs.io",
configRefreshMs: 60_000, // 0 disables polling
handleTermSignals: true, // SIGTERM/SIGINT flush + shutdown
defaultAttributes: { region: "us-east-1" },
serviceName: "checkout-api",
serviceVersion: "2.1.0",
deploymentVersion: process.env.GIT_SHA,
emitter: { maxBatchSize: 100, flushIntervalMs: 5_000 },
logger: myLogger,
fetchImpl: myFetch,
})API
| | |
| --- | --- |
| initVariantLabs(options) | Create a client |
| client.init() | Initial config fetch |
| 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 |
| resolveSdkContext(options) | Env-var resolution, exported standalone |
Delivery is lossy by design — a full queue drops events and a permanently failing batch is discarded. Telemetry never blocks or crashes your service.
Compatibility
Node >= 22. Ships ESM + CJS + type declarations.
License
Apache-2.0
