name: statsig-to-launchdarkly-migrator
description: Migrate JavaScript/TypeScript/React/Node.js code from the Statsig SDK to LaunchDarkly. Use whenever the user mentions migrating off Statsig, replacing Statsig with LaunchDarkly, porting feature gates or dynamic configs to LaunchDarkly, converting StatsigUser to LDContext, or moving Statsig session replay / autocapture to LaunchDarkly Observability. Also use when a user pastes Statsig SDK code and asks for the LaunchDarkly equivalent. Handles feature gates and dynamic configs only — experiments are flagged and left in place. CRITICAL: agentic tools routinely import a stale LaunchDarkly Node SDK pulled from training data; this skill always resolves the current version via live npm lookup before writing any code.
Statsig → LaunchDarkly SDK Migrator
Convert Statsig SDK code (JavaScript, TypeScript, React, Node.js) to LaunchDarkly while preserving experiments, ensuring the latest SDK versions, and writing the LD Client-Side ID to .env via ldcli. Migrates feature gates and dynamic configs; preserves and flags experiments.
This skill rewrites application code only. Flag, metric, and targeting definitions live in Statsig's backend, not in code — they're migrated by the statsig-to-ld CLI that ships in the same repo. See Beyond SDK code at the bottom for how the two surfaces compose, or the repo's README for the orchestration view.
When this skill runs
Trigger phrases include: "migrate from Statsig", "replace Statsig with LaunchDarkly", "port these flags to LaunchDarkly", "convert StatsigUser to LDContext", or pasted Statsig code with a request to translate it.
Workflow
Run these phases in order. Do not skip phases.
Phase 0 — Inventory
- Scan the codebase for:
statsig-js, @statsig/react-bindings, @statsig/web-analytics, @statsig/session-replay, statsig-node.
- List every
checkGate, getConfig, useGateValue, useConfig, getExperiment, useExperiment, useLayer call.
- List every
Statsig.initialize / StatsigProvider / StatsigClient construction site and the user/context object passed.
- Experiment gate: if
getExperiment / useExperiment / useLayer is found, mark those flags BLOCKED. The Statsig SDK stays. See references/experiments.md.
Output a short inventory table to the user before proceeding.
Phase 1 — Resolve latest SDK versions (REQUIRED)
Do NOT write any package.json edits or import statements until this phase completes. Agentic tools — including Claude — routinely emit stale LaunchDarkly Node SDK versions pulled from training data (launchdarkly-node-server-sdk@5.x is the most common stale name; the current package is @launchdarkly/node-server-sdk). Always resolve via live lookup.
Run:
node skills/statsig-to-launchdarkly-migrator/scripts/resolve-versions.mjs
The script writes migration-versions.json with the current published version of each LaunchDarkly package the migration touches. Use those versions in package.json and cite them in the migration summary. If the script fails (offline), fall back to references/version-floors.md and surface a warning to the user.
Phase 2 — Pull the LaunchDarkly Client-Side ID via ldcli
The migrated code MUST read the Client-Side ID from .env, never a string literal. Follow references/sdk-key-setup.md — it handles:
ldcli install detection + install (brew or curl)
ldcli login (interactive — the token does not pass through Claude)
- Project / environment selection
- Writing
LD_CLIENT_SIDE_ID=... (or LD_SDK_KEY=... for server SDKs) to .env / .env.local
- Ensuring
.env is in .gitignore
- Never echoing the key back to the conversation
Never reuse the Statsig SDK key. Statsig keys (client-...) and LaunchDarkly Client-Side IDs are not interchangeable; the migrated code will silently fail against LD if you wire the wrong one. If a Statsig key string survives in the migrated output, the static evals will fail.
Phase 3 — Translate code
Translate in this order: imports → initialization → context → flag evaluations → observability. Canonical patterns:
Imports
const statsig = require('statsig-js');
import statsig from 'statsig-js';
import { StatsigProvider, useGateValue } from '@statsig/react-bindings';
import { StatsigClient } from 'statsig-node';
Node.js call-out: the current package is @launchdarkly/node-server-sdk (scoped). The legacy unscoped name launchdarkly-node-server-sdk is the artifact of stale training data — if it appears in any migrated file, the static evals fail.
Initialization (read key from env, wait for init, latest version)
import { initialize } from 'launchdarkly-js-client-sdk';
const client = initialize(
process.env.LD_CLIENT_SIDE_ID,
context,
{ }
);
try {
await client.waitForInitialization(5);
} catch (err) {
}
For Node.js use init(process.env.LD_SDK_KEY) from @launchdarkly/node-server-sdk.
Flag evaluation
| Statsig | LaunchDarkly | Notes |
|---|
statsig.checkGate("gate_name") | client.variation("gate_name", false) | Fallback MUST be false. |
statsig.getConfig("cfg").get("title", "Default") | client.jsonVariation("cfg", { title: "Default" }).title | Provide a COMPLETE fallback object. |
useGateValue("gate_name") | useFlags().gateName | React auto-camelCases. |
useConfig("homepage_config") | useFlags().homepageConfig | React auto-camelCases. |
For typed SDKs (Node, server-side), prefer boolVariation / stringVariation / numberVariation / jsonVariation over the generic variation. Never use null or undefined as a fallback — see references/flag-evaluation.md.
Context (Statsig user → LDContext)
{ userID: "u-1", email: "a@b.com", custom: { tier: "pro" }, privateAttributes: { ssn: "xxx" } }
{
kind: "user",
key: "u-1",
email: "a@b.com",
tier: "pro",
ssn: "xxx",
_meta: { privateAttributes: ["ssn"] }
}
Full mapping (multi-context, customIDs, IP/userAgent inference) in references/context-migration.md.
Observability (only if Statsig session replay or autocapture is imported)
If @statsig/web-analytics or @statsig/session-replay appears, port to LaunchDarkly's Observability and SessionReplay plugins. If neither is imported, do not add any plugins — keep the init call lean. Full parameter mapping in references/observability.md.
Phase 4 — Flag naming by SDK
LaunchDarkly SDKs handle flag names differently:
| SDK | Auto camelCase? | Example |
|---|
| React | Yes (default) | admin_panel_access → flags.adminPanelAccess |
| JavaScript / Node / Python / Go / Java / iOS / Android | No | admin_panel_access stays admin_panel_access |
React-only: can disable with reactOptions: { useCamelCaseFlagKeys: false }. The migration summary must record both names for every flag so reviewers can cross-check.
Phase 5 — Half-implement, then test
Do not flip to LaunchDarkly in prod. The release shape is:
- In LaunchDarkly UI/API: create each migrated flag in the test environment. Set it OFF for everyone.
- In code: ship the migrated SDK init + variation calls. Statsig keys are gone or fenced behind experiment-only paths.
- In test env: deploy. Hit a route that exercises a migrated flag. Confirm via LD's Live Events that the expected context arrived and the expected variation was served (
ldcli flags get <key> --env test cross-checks).
- In test env: flip the flag on, verify the new code path actually executes.
- Only then repeat steps 1, 3, 4 in prod environment.
Phase 6 — Verification (the user runs this)
Before declaring success, surface this checklist to the user verbatim:
Phase 7 — Generate the migration report
Write migration-summary.json to the project root. See references/report-format.md for the schema. Required fields: resolved SDK versions, list of migrated flags with both raw and camelCased names, list of blocked-by-experiment flags, list of failed items with reasons, observability migration delta (Statsig params lost in translation), and the verification checklist.
What does NOT get migrated
- Experiments (
getExperiment, useExperiment, useLayer) — preserved as-is, both SDKs run in parallel
- Feature gates that are part of an experiment — blocked, surfaced in the report
- Complex targeting rules — must be recreated in the LD dashboard manually
- Statsig-specific session replay parameters (
maxSessionDurationMs, recordConsoleErrors) — no LaunchDarkly equivalent
Evals
Two eval harnesses live under evals/:
evals/static/ — runs the skill against fixtures and asserts on the output (versions, imports, fallbacks, context shape, no leaked Statsig keys). CI-runnable: node evals/static/run.mjs.
evals/judge/ — LLM-as-judge scoring against a rubric. Slower and costlier; run manually before releasing skill changes: node evals/judge/run.mjs.
See evals/README.md for details and rubric.
Output format the skill produces
- Inventory table (Phase 0)
- Resolved SDK versions table (Phase 1)
- SDK key setup confirmation — never the key value itself (Phase 2)
- Per-file migrated code blocks (Phase 3)
- Flag-naming summary by SDK (Phase 4)
- Test environment rollout checklist (Phase 5)
- Verification checklist for the user (Phase 6)
migration-summary.json written to project root (Phase 7)
Hard rules
- Never reuse a Statsig SDK key
- Never write a literal Client-Side ID in code — always
process.env.*
- Never use
null or undefined as a flag fallback
- Never emit
launchdarkly-node-server-sdk (legacy unscoped name) — the current package is @launchdarkly/node-server-sdk
- Never migrate experiments
- Never skip Phase 1 (version resolution) — stale SDK versions are the #1 reliability failure for agentic migrations
Beyond SDK code
This skill rewrites application code. It does not create LaunchDarkly flags, metrics, segments, or targeting rules in the LaunchDarkly backend — those are server-side definitions that live alongside, not inside, your code. The companion statsig-to-ld CLI (same repo) handles those:
| You want to migrate… | Use |
|---|
| Statsig SDK call sites in JS / TS / React / Node code | This skill. |
| LaunchDarkly flag shells corresponding to your Statsig gates and dynamic configs | statsig-to-ld flags import |
| Per-environment targeting rules, rollouts, and overrides | statsig-to-ld targeting import |
| Statsig metric definitions → LD metrics | statsig-to-ld metrics convert |
| Statsig warehouse-native experimentation setup: Snowflake / BigQuery / Databricks / Redshift integrations + LD metric data sources | statsig-to-ld warehouse (writes source-mapping.json) |
Metric definitions for warehouse-native (each bound to a data source warehouse created) | statsig-to-ld metrics convert --source-mapping source-mapping.json |
| Scoping the work before any of the above | statsig-to-ld analyze |
Operator detail for the CLI lives in AGENTS.md at the repo root. Project-level decisions the automated surfaces don't make for you (context-kind mapping, segment recreation, experiment design, validation strategy, cutover, rollback) live in docs/migration-playbook.md.
The migration-summary.json this skill emits (Phase 7) lists canonical flag keys — feed it to statsig-to-ld flags import so the flag shells use the same keys your migrated code reads.
Reference docs