Skip to content
Gauge

ax-check.com

LaunchDarkly

launchdarkly.com

ax-check.com

Checked 14 October 2026

B

65/100

Agents need your login

LaunchDarkly's quickstart and docs are agent-ready, with clear pricing.

18 of 23 checks passed: install steps, code samples, API/MCP docs and pricing are all readable without login. The homepage just lacks a machine-readable index.

3 fixes to start with

Concrete changes, backed by what we found.

Onboarding

Give agents a clear login handoff and a way to resume setup.

Agents cannot progress through onboarding without human help. Provide an agent-accessible path through authentication and setup.

Docs

Return Markdown when clients request text/markdown.

Homepage returned text/html even when text/markdown was requested; no Markdown representation offered.

Docs

Add links to your product documentation and quickstart in /llms.txt.

https://launchdarkly.com/llms.txt returns HTTP 404, so no documentation index exists at the standard path.

Recorded coding sessions

Same task, 3 models, no launchdarkly.com account. Recorded 14 October 2026 on Pi.

3 recorded · 4m 22s

4m 22s · recorded

Session findings

Select a finding to see it in context.

piv0.84.2
modelQwen 3.8 Maxcwd/sandbox/repo
Help me build a simple example using LaunchDarkly. Tell me how pricing works, and briefly tell me whether this product will be easy for you to manage. Let me know if you get blocked. If this product has no developer workflow you can act on, say so plainly and stop. Stay light: use the hosted product through its SDK or API. Do not start local service stacks or wait for long-running commands; if the quickstart requires either, say so plainly and stop.
Thinking…

I need to check what's in the current directory to understand what we're working with.

ls -la /sandbox/repo && cat /sandbox/repo/README* 2>/dev/null | head -50
total 16
total 16
drwxr-xr-x 3 root root 4096 Sep 18 08:00 .
drwxr-xr-x 7 root root 4096 Oct 14 16:45 ..
drwxr-xr-x 8 root root 4096 Sep 24 20:05 .git
-rw-r--r-- 1 root root  408 Sep 18 08:00 .gitignore
which node python3 curl; node --version 2>/dev/null; python3 --version 2>/dev/null; env | grep -i -E "launchdarkly|ld_" | sed 's/=.*/=<set>/'
/usr/local/bin/node
/usr/local/bin/node
/usr/local/bin/python3
/usr/bin/curl
v22.23.2
Python 3.12.3
Thinking…

Empty repo, no LaunchDarkly credentials. LaunchDarkly is a hosted feature-flag product with a real developer workflow: SDKs (Node server SDK, JS client SDK), REST API for flags, Relay Proxy, etc. So I can act on it: write an SDK example, plus an API script to create flags. But without an SDK key/access token, I can't run it against real LD. I can write the example and a stub/offline mode demo (LD Node SDK supports offline mode; also there's ldClient.variation with defaults). I could also run offline mode locally — that's just a library, not a service stack. That's allowed ("Stay light").

Let me check network availability and whether npm install works.

Plan:

  1. Check network + npm.
  2. Create a small Node example using launchdarkly-node-server-sdk with offline mode fallback, plus a flag-management script using the LD REST API (requires LD_ACCESS_TOKEN).
  3. Report pricing info (need to be accurate: LaunchDarkly pricing — Developer plan free? Let me recall. As of 2024/2025: LaunchDarkly has plans: Developer (free, up to 3 users/1 project?), Standard, Enterprise. Pricing based on monthly active users (MAU) / monthly context instances, seats. Free Developer plan: 1 project, 1 environment, up to 10 seats? Hmm. I should be careful and possibly fetch pricing page if network available.

Let me test network.

cd /tmp && timeout 20 curl -sS -o /dev/null -w "npm:%{http_code}\n" https://registry.npmjs.org/launchdarkly-node-server-sdk; timeout 20 curl -sS -o /dev/null -w "launchdarkly.com:%{http_code}\n" https://launchdarkly.com/pricing/ ; timeout 20 curl -sS -o /dev/null -w "app.launchdarkly.com:%{http_code}\n" https://app.launchdarkly.com
npm:200
npm:200
launchdarkly.com:200
app.launchdarkly.com:200
Thinking…

Network works. Let me fetch pricing page content briefly to be accurate.

cd /tmp && timeout 30 curl -sSL https://launchdarkly.com/pricing/ -o ldpricing.html && wc -c ldpricing.html && python3 - <<'EOF'
import re,html
t=open('/tmp/ldpricing.html',encoding='utf-8',errors='ignore').read()
t=re.sub(r'(?s)<(script|style|noscript|svg).*?</\1>',' ',t)
t=re.sub(r'(?s)<[^>]+>',' ',t)
t=html.unescape(t)
t=re.sub(r'[ \t]+',' ',t)
lines=[l.strip() for l in t.split('\n') if l.strip()]
out='\n'.join(lines)
print(out[:6000])
EOF
355179 ldpricing.html
355179 ldpricing.html
Pricing | LaunchDarkly [Virtual event] Automate Your Software Factory - Nov 5 — Save my seat Skip to: Home Content Footer navigation Get a demo Platform CodeControl Ship confidently, with automated control built in. AgentControl One place to keep your agent behavior under control. Book a demo How it works Platform overview Feature flags Observability Experimentation Agent integrations Integrations Architecture Solutions By Use Case Release AI-built code Control AI agents Optimize AI performance & cost Self-heal systems Run experiments By Team AI engineers Developers DevOps & SRE Product managers Data scientists Resources Learn Blog Guides & ebooks Events Videos Success Academy Customer stories Professional services Partners Get Help Help Center Explore help topics for everything LaunchDarkly. Request support Reach out to us for direct answers to your questions. Developers Docs Docs home Feature flags Quickstart AgentControl Quickstart API docs Resources Product Updates Power analysis calculator Flagship engineering blog Community Take control of your code and agents at runtime. Control releases, experiments, and AI behavior in prod. Free trial Pricing Sign in Book a demo Book a demo Flexible pricing for every stage and team. Developer One platform for features and agents. Free to start. Get started free No credit card required $0 / mo, forever Unlimited seats No per-seat pricing. Invite your whole team; pricing is based on usage. A/B Tests and Experiments Includes 100K experimentation MAU per month at no additional charge. 10M logs and 10M traces /mo A log is a single log line and a trace is a single span, each ingested, stored, and searchable for one month. CodeControl: Unlimited feature flags 30 idiomatic SDKs 13 client-side, 13 server-side, and 4 edge SDKs. 5 service connections and 1K client-side MAU /mo A service connection is one server-side SDK connected to one environment for a month. Each unique client-side user or device is one MAU. 5K session replays and 5K errors /mo A session replay is one recorded user session (a single tab instance); an error is one unique error or exception. Each is stored for one month. AgentControl: Configure, evaluate, roll out, and observe your AI end to end 5k AI runs/mo included An AI Run is generated when you track an LLM request or run an online eval (LLM-as-a-judge). 14 days data retention How long AgentControl run and eval data stays available. Foundation Ship features and agents to production. Scale both without hitting a ceiling. Pay as you go 14-day trial, then pay as you go. No platform or seat fees. Pay as you go Break it down Billed yearly $10 per Service Connection / mo One server-side SDK connected to one environment for a month. Serverless and short-lived processes bill fractionally by connection time. $8.33 per 1k client-side MAU / mo Each unique client-side user or device counts as one monthly active user. 5k AI Runs included / mo. An AI run is generated when you track an LLM request or run an online eval (LLM-as-a-judge). $5 per additional 1k runs. Special rates for K8s, Serverless, Python, & Ruby Because these architectures create a high volume of server-side connections, standard per-connection pricing isn't a fair fit. We'll tailor a package to your environment, so the way you run doesn't inflate your bill. Get custom pricing Everything in Developer, plus: Unlimited projects and environments 5 service connections included, pay as you go beyond Your first 5 service connections each month are included. Additional connections bill at $10 each per month. Scalable observability usage 5K session replays, 5K errors, 10M logs, and 10M traces included each month. Usage beyond that bills as you go. AgentControl: Usage-based pricing past 5,000 AI runs/mo An AI Run is generated when you track an LLM request or run an online eval (LLM-as-a-judge). 30 days data retention How long AgentControl run and eval data stays available. Enterprise Full Platform controls and security for engineering teams at scale. Get custom pricing Get Enterprise trial Custom pricing Everything in Foundation, plus: Advanced user targeting Bulk targeting plus large list-based and synced segments. Custom roles & teams Fine-grained permissions with custom roles, organized into teams. Workflows, scheduling, & approvals Approval requests, scheduled flag changes, and multi-step release workflows. Release automation Release Assistant SCIM user provisioning Automated user provisioning and deprovisioning through your identity provider. SSO and SAML are included on every plan. AgentControl: Custom number of AI runs An AI Run is generated when you track an LLM request or run an online eval (LLM-as-a-judge). 100 days data retention How long AgentControl run and eval data stays available. Guardian Paid add-on Guardian adds release safety on top of Enterprise: monitoring, guardrail metrics, and automatic rollback. Priced separately. Release Monitoring Guardrail Metrics Metrics watched during every rollout to catch regressions early. Proactive Failure Notifications Automatic Pause or Rollback Advanced Observability Exposure Insights See which users and segments were exposed to each release and how it affected them. Compare all features Developer Get started free Start free Foundation Pay as you go Pay as you go Enterprise Get custom pricing Get custom pricing Platform AI Runs / month 5000 / mo 5000 / mo + overages Custom (contracted) Data / trace retention 14 days 30 days 100+ days Config delivery latency (SLO) < 300ms < 200ms < 150ms (SLA) Overage behavior Ingestion stops at 5K $5 / 1000 Contracted true-up Add-on AI Run blocks — $5 / 1000 Volume discounts available Service Connections (SCs) Limited (free tier) SC-based billing SC-based billing Client-side MAU Limited (free tier) $10 / mo per 1K MAU Custom Projects & environments 1 project & 3 environments Unlimited Unlimited Agent configuration Prompts & model configs Prompt Snippets Tools Library Agent Graphs Feature management Bool
Thinking…

Great, I have accurate pricing details. Note a discrepancy: free tier says "5 service connections and 1K client-side MAU/mo", Foundation says "$10 per Service Connection" and "$8.33 per 1k client-side MAU" (compare table says $10/mo per 1K MAU — slight inconsistency on their page; I'll quote both carefully).

Now build the example. No LD credentials in env. I'll build:

  • Node example using launchdarkly-node-server-sdk with offline mode so it runs without credentials (no long-running service; SDK offline init is instant).
  • A small REST API helper script (create/update flag) that works if LD_ACCESS_TOKEN is set — I'll verify it errors gracefully without token. I shouldn't actually call the API without credentials (can't).
  • Maybe also show client-side JS SDK example.

Check npm install speed for launchdarkly-node-server-sdk. Let's do it.

Also verify docs quickstart accuracy: Node server SDK v9/10: import * as LaunchDarkly from 'launchdarkly-node-server-sdk'; const client = LaunchDarkly.init(sdkKey); await client.waitForInitialization(); client.variation('flag-key', context, default). Contexts: { kind: 'user', key: 'user-123' }. Offline: LaunchDarkly.init(sdkKey, { offline: true }). In v9+, init returns LDClient; waitForInitialization(). Let me check the installed version's API.

Let's install.

cd /sandbox/repo && cat .gitignore && mkdir -p launchdarkly-example && cd launchdarkly-example && npm init -y >/dev/null 2>&1 && timeout 180 npm install launchdarkly-node-server-sdk 2>&1 | tail -5 && node -e "console.log(require('launchdarkly-node-server-sdk/package.json').version)"
# Keep generated and vendored files out of source diffs.
# Keep generated and vendored files out of source diffs.

# Logs / OS cruft
*.log
.DS_Store

# JavaScript / TypeScript
node_modules/
.next/
dist/
build/
coverage/

# Python
.venv/
venv/
__pycache__/
*.py[cod]
.pytest_cache/
*.egg-info/

# Rust
target/

# Go / PHP / Ruby vendored deps
vendor/
.bundle/

# Java / Kotlin / Scala
.gradle/

# .NET
bin/
obj/

# Elixir
_build/
deps/

# Dart / Flutter
.dart_tool/

To address all issues (including breaking changes), run:
  npm audit fix --force

Run `npm audit` for details.
7.0.4
Thinking…

SDK v7.0.4 — newer major. Let me check the API surface of v7: init, LDClient, variation, offline mode. Let me inspect the package's typings/README.

cd /sandbox/repo/launchdarkly-example && ls node_modules/launchdarkly-node-server-sdk && ls node_modules/launchdarkly-node-server-sdk/dist 2>/dev/null | head -30 && head -60 node_modules/launchdarkly-node-server-sdk/README.md
CHANGELOG.md
CHANGELOG.md
CODEOWNERS
CONTRIBUTING.md
LICENSE.txt
README.md
SECURITY.md
attribute_reference.js
big_segments.js
caching_store_wrapper.js
configuration.js
context.js
context_filter.js
contract-tests
diagnostic_events.js
docs
errors.js
evaluator.js
event_factory.js
event_processor.js
event_summarizer.js
feature_store.js
feature_store_event_wrapper.js
file_data_source.js
flags_state.js
index.d.ts
index.js
integrations.js
interfaces.js
loggers.js
messages.js
operators.js
package.json
polling.js
requestor.js
scripts
sharedtest
streaming.js
test
test-types.ts
test_data.js
tsconfig.json
update_queue.js
utils
versioned_data_kind.js
# LaunchDarkly Server-Side SDK for Node.js

# Use [@launchdarkly/node-server-sdk](https://www.npmjs.com/package/@launchdarkly/node-server-sdk) instead of this package.

As mentioned in the [repository changelog](https://github.com/launchdarkly/node-server-sdk/blob/main/CHANGELOG.md), the `launchdarkly-node-server-sdk` project has been renamed to `@launchdarkly/node-server-sdk`. All future releases will be made from the [new repository](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/server-node). Please consider upgrading and filing potential requests in that repository's [issue tracker](https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+sdk%2Fserver-node%22+sort%3Aupdated-desc).

# ☝️☝️☝️☝️☝️☝️

## v7.x readme

[![NPM](https://img.shields.io/npm/v/launchdarkly-node-server-sdk.svg?style=flat-square)](https://www.npmjs.com/package/launchdarkly-node-server-sdk)
[![CircleCI](https://circleci.com/gh/launchdarkly/node-server-sdk.svg?style=svg)](https://circleci.com/gh/launchdarkly/node-server-sdk)
[![Documentation](https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8)](https://launchdarkly.github.io/node-server-sdk)

The LaunchDarkly Server-Side SDK for Node.js is designed primarily for use in multi-user systems such as web servers and applications. It follows the server-side LaunchDarkly model for multi-user contexts. It is not intended for use in desktop and embedded systems applications.

For using LaunchDarkly in *client-side* Node.js applications, refer to our [Client-side Node.js SDK](https://github.com/launchdarkly/node-client-sdk).

## LaunchDarkly overview

[LaunchDarkly](https://www.launchdarkly.com) is a feature management platform that serves trillions of feature flags daily to help teams build better software, faster. [Get started](https://docs.launchdarkly.com/home/getting-started) using LaunchDarkly today!

[![Twitter Follow](https://img.shields.io/twitter/follow/launchdarkly.svg?style=social&label=Follow&maxAge=2592000)](https://twitter.com/intent/follow?screen_name=launchdarkly)

## Supported Node versions

This version of the LaunchDarkly SDK is compatible with Node.js versions 12 and above.

## Getting started

Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/server-side/node-js) for instructions on getting started with using the SDK.

## Learn more

Read our [documentation](http://docs.launchdarkly.com) for in-depth instructions on configuring and using LaunchDarkly. You can also head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/node-js).

The authoritative description of all properties and methods is in the [TypeScript documentation](https://launchdarkly.github.io/node-server-sdk/).

## Testing

We run integration tests for all our SDKs using a centralized test harness. This approach gives us the ability to test for consistency across SDKs, as well as test networking behavior in a long-running application. These tests cover each method in the SDK, and verify that event sending, flag evaluation, stream reconnection, and other aspects of the SDK all behave correctly.

## Contributing

We encourage pull requests and other contributions from the community. Check out our [contributing guidelines](CONTRIBUTING.md) for instructions on how to contribute to this SDK.

## About LaunchDarkly

* LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard.  With LaunchDarkly, you can:
    * Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases.
    * Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?).
    * Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file.
    * Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). Disable parts of your application to facilitate maintenance, without taking everything offline.
* LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list.
* Explore LaunchDarkly
    * [launchdarkly.com](https://www.launchdarkly.com/ "LaunchDarkly Main Website") for more information
    * [docs.launchdarkly.com](https://docs.launchdarkly.com/  "LaunchDarkly Documentation") for our documentation and SDK reference guides
    * [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/  "LaunchDarkly API Documentation") for our API documentation
    * [blog.launchdarkly.com](https://blog.launchdarkly.com/  "LaunchDarkly Blog Documentation") for the latest product updates
Thinking…

The legacy package is deprecated; the new one is @launchdarkly/node-server-sdk. Let's install that instead. Also the new SDK supports TestData data source (integrations.TestData) — perfect for offline/local demo without credentials, and offline mode.

Install @launchdarkly/node-server-sdk.

cd /sandbox/repo/launchdarkly-example && npm uninstall launchdarkly-node-server-sdk >/dev/null 2>&1 && timeout 180 npm install @launchdarkly/node-server-sdk 2>&1 | tail -3 && node -e "const p=require('@launchdarkly/node-server-sdk/package.json');console.log(p.version)"
Failedadded 11 packages, and audited 12 packages in 3s
added 11 packages, and audited 12 packages in 3s

found 0 vulnerabilities
node:internal/modules/cjs/loader:671
      throw e;
      ^

Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath './package.json' is not defined by "exports" in /sandbox/repo/launchdarkly-example/node_modules/@launchdarkly/node-server-sdk/package.json
    at exportsNotFound (node:internal/modules/esm/resolve:314:10)
    at packageExportsResolve (node:internal/modules/esm/resolve:661:9)
    at resolveExports (node:internal/modules/cjs/loader:664:36)
    at Function._findPath (node:internal/modules/cjs/loader:731:31)
    at Function._resolveFilename (node:internal/modules/cjs/loader:1415:27)
    at defaultResolveImpl (node:internal/modules/cjs/loader:1040:19)
    at resolveForCJSWithHooks (node:internal/modules/cjs/loader:1045:22)
    at Function._load (node:internal/modules/cjs/loader:1216:25)
    at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
    at Module.require (node:internal/modules/cjs/loader:1527:12) {
  code: 'ERR_PACKAGE_PATH_NOT_EXPORTED'
}

Node.js v22.23.2


Command exited with code 1
Thinking…

Fine — just exports restriction. Check version from file and API surface (ESM). Let's look at index.d.ts.

cd /sandbox/repo/launchdarkly-example && node -p "JSON.parse(require('fs').readFileSync('node_modules/@launchdarkly/node-server-sdk/package.json')).version" && node -p "JSON.stringify(JSON.parse(require('fs').readFileSync('node_modules/@launchdarkly/node-server-sdk/package.json')).exports,null,1)" && ls node_modules/@launchdarkly/node-server-sdk/dist/esm/src 2>/dev/null | head
9.14.4
9.14.4
{
 ".": {
  "types": "./dist/src/index.d.ts",
  "default": "./dist/src/index.js"
 },
 "./integrations": {
  "types": "./dist/src/integrations.d.ts",
  "default": "./dist/src/integrations.js"
 }
}
Thinking…

Wait — dist/src/index.js — is it CJS or ESM? package.json "type"? Let me check. Then check integrations exports (TestData, FileData).

Let me grep index.d.ts exports.

cd /sandbox/repo/launchdarkly-example/node_modules/@launchdarkly/node-server-sdk && node -p "JSON.parse(require('fs').readFileSync('package.json')).type||'commonjs'" && cat dist/src/index.d.ts | head -80 && echo ---- && cat dist/src/integrations.d.ts | head -40
commonjs
commonjs
/**
 * This is the API reference for the LaunchDarkly Server-Side SDK for Node.js.
 *
 * In typical usage, you will call {@link init} once at startup time to obtain an instance of
 * {@link LDClient}, which provides access to all of the SDK's functionality.
 *
 * For more information, see the SDK reference guide.
 *
 * @packageDocumentation
 */
import { BasicLoggerOptions, LDLogger } from '@launchdarkly/js-server-sdk-common';
import { LDClient } from './api/LDClient';
import { LDOptions } from './api/LDOptions';
export * from '@launchdarkly/js-server-sdk-common';
export { LDOptions };
export { LDClient, BigSegmentStoreStatusProvider } from './api';
/**
 * Creates an instance of the LaunchDarkly client.
 *
 * Applications should instantiate a single instance for the lifetime of the application.
 * The client will begin attempting to connect to LaunchDarkly as soon as it is created. To
 * determine when it is ready to use, call {@link LDClient.waitForInitialization}, or register an
 * event listener for the `"ready"` event using {@link LDClient.on}.
 *
 * **Important:** Do **not** try to instantiate `LDClient` with its constructor
 * (`new LDClient()/new LDClientImpl()/new LDClientNode()`); the SDK does not currently support
 * this.
 *
 * @param key
 *   The SDK key.
 * @param options
 *   Optional configuration settings.
 * @return
 *   The new {@link LDClient} instance.
 */
export declare function init(sdkKey: string, options?: LDOptions): LDClient;
/**
 * Provides a simple {@link LDLogger} implementation.
 *
 * This logging implementation uses a simple format that includes only the log level
 * and the message text. Output is written to the standard error stream (`console.error`).
 * You can filter by log level as described in [[BasicLoggerOptions.level]].
 *
 * To use the logger created by this function, put it into {@link LDOptions.logger}. If
 * you do not set {@link LDOptions.logger} to anything, the SDK uses a default logger
 * that is equivalent to `basicLogger({ level: 'info' })`.
 *
 * @param options Configuration for the logger. If no options are specified, the
 *   logger uses `{ level: 'info' }`.
 *
 * @example
 * This example shows how to use `basicLogger` in your SDK options to enable console
 * logging only at `warn` and `error` levels.
 * ```javascript
 *   const ldOptions = {
 *     logger: basicLogger({ level: 'warn' }),
 *   };
 * ```
 *
 * @example
 * This example shows how to use `basicLogger` in your SDK options to cause log
 * output to go to `console.log` instead of `console.error`.
 * ```javascript
 *   const ldOptions = {
 *     logger: ld.basicLogger({ destination: console.log }),
 *   };
 * ```
 */
export declare function basicLogger(options: BasicLoggerOptions): LDLogger;
//# sourceMappingURL=index.d.ts.map----
export { FileDataSourceFactory, TestData, TestDataFlagBuilder, TestDataRuleBuilder, } from '@launchdarkly/js-server-sdk-common';
//# sourceMappingURL=integrations.d.ts.map
Thinking…

Good. Now build example files:

  1. src/index.js (CJS) — main example: init client with SDK key from env LD_SDK_KEY; if absent, use offline mode + TestData so it runs locally with zero credentials. Evaluate boolean flag show-new-checkout, string/JSON flag, targeting rules based on context attributes, and variationDetail to show reason. Also listen for flag change events. Exit after (no long-running process) — but include a note about keeping alive in production.

Let me structure:

  • src/app.js: business logic function renderCheckout(context, client).
  • src/index.js: sets up client, runs demo, closes.
  • scripts/manage-flag.js: uses LD REST API (Code References / Flags API) to create + update flag rollout percentage; requires LD_ACCESS_TOKEN + LD_PROJECT_KEY. Without token, prints instructions and exits (no network call).
  • flags/flags.json + FileDataSource option for local dev.
  • README.md.
  • .env.example.

TestData API in js-server-sdk-common: TestData.dataSource(), td.update(td.flag('key').booleanFlag().variationForAll(true)), .fallthroughVariation, .ifMatch(...), .thenServe. And init(sdkKey, { updateProcessor: td }). Also offline: { offline: true }.

Let me verify TestData builder method names in the installed common package.

cd /sandbox/repo/launchdarkly-example && sed -n 1,200p node_modules/@launchdarkly/js-server-sdk-common/dist/src/integrations/test_data/TestData.d.ts
Failedsed: can't read node_modules/@launchdarkly/js-server-sdk-common/dist/src/integrations/test_data/TestData.d.ts: No such file or directory
sed: can't read node_modules/@launchdarkly/js-server-sdk-common/dist/src/integrations/test_data/TestData.d.ts: No such file or directory


Command exited with code 2
Thinking…

I need to locate the TestData.d.ts file.

cd /sandbox/repo/launchdarkly-example && find node_modules/@launchdarkly/js-server-sdk-common -iname "*TestData*" | head && echo --- && find node_modules/@launchdarkly/js-server-sdk-common -name "integrations.d.ts" -exec cat {} \;
---
---
Thinking…

Hmm, no files? Maybe the common package is bundled. Let me check the package structure.

cd /sandbox/repo/launchdarkly-example && ls node_modules/@launchdarkly/ && ls node_modules/@launchdarkly/js-server-sdk-common 2>/dev/null && find node_modules/@launchdarkly -maxdepth 3 -name "*.d.ts" | head -20
js-sdk-common
js-sdk-common
js-server-sdk-common
node-server-sdk
CHANGELOG.md
LICENSE
README.md
dist
package.json
node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts
Thinking…

All bundled into dist/index.d.ts. Let me grep for TestData class declaration in it.

cd /sandbox/repo/launchdarkly-example && grep -n "declare class TestData" -A 60 node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts | head -100
2475:declare class TestDataRuleBuilder<BuilderType> {
2475:declare class TestDataRuleBuilder<BuilderType> {
2476-    private readonly _flagBuilder;
2477-    private _clauses;
2478-    private _variation?;
2479-    /**
2480-     * Adds another clause using the "is one of" operator.
2481-     *
2482-     * For example, this creates a rule that returns `true` if the name is
2483-     * "Patsy" and the country is "gb":
2484-     *
2485-     *     testData.flag('flag')
2486-     *             .ifMatch('name', 'Patsy')
2487-     *             .andMatch('country', 'gb')
2488-     *             .thenReturn(true)
2489-     *
2490-     * @param contextKind the kind of the context
2491-     * @param attribute the user attribute to match against
2492-     * @param values values to compare to
2493-     * @return the flag rule builder
2494-     */
2495-    andMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder<BuilderType>;
2496-    /**
2497-     * Adds another clause using the "is not one of" operator.
2498-     *
2499-     * For example, this creates a rule that returns `true` if the name is
2500-     * "Patsy" and the country is not "gb":
2501-     *
2502-     *     testData.flag('flag')
2503-     *             .ifMatch('name', 'Patsy')
2504-     *             .andNotMatch('country', 'gb')
2505-     *             .thenReturn(true)
2506-     *
2507-     * @param contextKind the kind of the context
2508-     * @param attribute the user attribute to match against
2509-     * @param values values to compare to
2510-     * @return the flag rule builder
2511-     */
2512-    andNotMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder<BuilderType>;
2513-    /**
2514-     * Finishes defining the rule, specifying the result value as either a boolean or an index
2515-     *
2516-     * If the variation is a boolean value and the flag was not already a boolean
2517-     * flag, this also changes it to be a boolean flag.
2518-     *
2519-     * If the variation is an integer, it specifies a variation out of whatever
2520-     * variation values have already been defined.
2521-     *
2522-     * @param variation
2523-     *    either `true` or `false` or the index of the desired variation:
2524-     *    0 for the first, 1 for the second, etc.
2525-     * @return the flag rule builder
2526-     */
2527-    thenReturn(variation: number | boolean): BuilderType;
2528-}
2529-
2530-/**
2531- * A builder for feature flag configurations to be used with {@link TestData}.
2532- */
2533:declare class TestDataFlagBuilder {
2534-    private readonly _key;
2535-    private _data;
2536-    private get _isBooleanFlag();
2537-    /**
2538-     * A shortcut for setting the flag to use the standard boolean configuration.
2539-     *
2540-     * This is the default for all new flags created with {@link TestData.flag}. The
2541-     * flag will have two variations, `true` and `false` (in that order). It
2542-     * will return `false` whenever targeting is off and `true` when targeting
2543-     * is on unless other settings specify otherwise.
2544-     *
2545-     * @return the flag builder
2546-     */
2547-    booleanFlag(): TestDataFlagBuilder;
2548-    /**
2549-     * Sets the allowable variation values for the flag.
2550-     *
2551-     * The values may be of any JSON-compatible type: boolean, number, string, array,
2552-     * or object. For instance, a boolean flag normally has `variations(true, false)`;
2553-     * a string-valued flag might have `variations("red", "green")`; etc.
2554-     *
2555-     * @param values any number of variation values
2556-     * @return the flag builder
2557-     */
2558-    variations(...values: any[]): TestDataFlagBuilder;
2559-    /**
2560-     * Sets targeting to be on or off for this flag.
2561-     *
2562-     * The effect of this depends on the rest of the flag configuration, just
2563-     * as it does on the real LaunchDarkly dashboard. In the default configuration
2564-     * that you get from calling {@link TestData.flag} with a new flag key, the flag
2565-     * will return `false` whenever targeting is off and `true` when targeting
2566-     * is on.
2567-     *
2568-     * @param targetingOn true if targeting should be on
2569-     * @return the flag builder
2570-     */
2571-    on(targetingOn: boolean): TestDataFlagBuilder;
2572-    /**
2573-     * Specifies the fallthrough variation for a flag. The fallthrough is
2574-     * the value that is returned if targeting is on and the user was not
Thinking…

Good enough. Also check variationForAll, ifMatch, ifNotMatch, variationsForAll? Let me grep quickly.

cd /sandbox/repo/launchdarkly-example && grep -n "^    [a-zA-Z_]*(" node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts | sed -n '1,400p' | grep -n -i -E "variationForAll|ifMatch|ifNotMatch|fallthroughVariation|offVariation|dataSource|update\(" 
66:2458: constructor(_options: FileDataSourceOptions);
66:2458:    constructor(_options: FileDataSourceOptions);
67:2459:    getFactory(): (ldClientContext: LDClientContext, featureStore: LDFeatureStore, initSuccessHandler?: VoidFunction, errorHandler?: FileDataSourceErrorHandler) => subsystem.LDStreamProcessor;
74:2585:    fallthroughVariation(variation: number | boolean): TestDataFlagBuilder;
75:2598:    offVariation(variation: number | boolean): TestDataFlagBuilder;
76:2614:    variationForAll(variation: number | boolean): TestDataFlagBuilder;
82:2698:    ifMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder<TestDataFlagBuilder>;
83:2716:    ifNotMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder<TestDataFlagBuilder>;
88:2800:    update(flagBuilder: TestDataFlagBuilder): Promise<any>;
Thinking…

Now write the example. Files:

launchdarkly-example/

  • README.md
  • .env.example
  • flags/flags.json (file data source for local dev without network)
  • src/config.js — env config + client factory (real, offline, TestData)
  • src/app.js — feature-flagged business logic
  • src/index.js — runnable demo
  • scripts/manage-flag.js — REST API script (create flag, set rollout) — requires LD_ACCESS_TOKEN

Also add npm scripts. And a test? Keep light.

Check FileDataSource options format for flags.json: in js-core, FileDataSourceFactory({ flagData: { flags: { key: { on, variations, fallthroughVariation, targets... , offVariation } }, flagValues: {...} }, paths }). Let me check FileDataSourceOptions typing.

cd /sandbox/repo/launchdarkly-example && sed -n 2400,2470p node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts
variation?: number;
    variation?: number;
    rollout?: Rollout;
    trackEvents?: boolean;
    clauses?: Clause[];
}

interface Prerequisite {
    key: string;
    variation: number;
}

interface Target {
    contextKind?: string;
    values: string[];
    variation: number;
}

/**
 * Common interface for flags/segments. Versioned data we store.
 */
interface Versioned {
    key: string;
    version: number;
}

interface VariationOrRollout {
    variation?: number;
    rollout?: Rollout;
}
interface Flag extends Versioned {
    on: boolean;
    prerequisites?: Prerequisite[];
    targets?: Omit<Target, 'contextKind'>[];
    contextTargets?: Target[];
    rules?: FlagRule[];
    fallthrough: VariationOrRollout;
    offVariation?: number;
    variations: any[];
    clientSide?: boolean;
    clientSideAvailability?: ClientSideAvailability;
    salt?: string;
    trackEvents?: boolean;
    trackEventsFallthrough?: boolean;
    debugEventsUntilDate?: number;
    excludeFromSummaries?: boolean;
    samplingRatio?: number;
    migration?: {
        checkRatio?: number;
    };
}

type FileDataSourceErrorHandler = (err: LDFileDataSourceError) => void;

/**
 * Class for creating file data sources.
 */
declare class FileDataSourceFactory {
    private readonly _options;
    constructor(_options: FileDataSourceOptions);
    getFactory(): (ldClientContext: LDClientContext, featureStore: LDFeatureStore, initSuccessHandler?: VoidFunction, errorHandler?: FileDataSourceErrorHandler) => subsystem.LDStreamProcessor;
}

/**
 * A builder for feature flag rules to be used with {@link TestDataFlagBuilder}.
 *
 * In the LaunchDarkly model, a flag can have any number of rules, and
 * a rule can have any number of clauses. A clause is an individual test
 * such as "name is 'X'". A rule matches a user if all of the rule's
 * clauses match the user.
 *
 * To start defining a rule, use one of the flag builder's matching methods
Thinking…

Now write files. I'll use flagValues format for the file data source (simplest): { "flagValues": { "flag-key": value } } — but that doesn't show targeting. I'll use flags with full definitions for one flag to demo rules... Actually keep it simple: use TestData for the demo (no credentials needed, shows targeting rules programmatically), and include flags.json with flagValues for the "file data source" mode as an alternative. Let me verify FileDataSourceOptions supports flagValues.

grep for FileDataSourceOptions interface.

cd /sandbox/repo/launchdarkly-example && grep -n "interface FileDataSourceOptions" -A 40 node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts
2338:interface FileDataSourceOptions {
2338:interface FileDataSourceOptions {
2339-    /**
2340-     * The path(s) of the file(s) that FileDataSource will read.
2341-     */
2342-    paths: Array<string>;
2343-    /**
2344-     * True if FileDataSource should reload flags whenever one of the data files is modified.
2345-     * This feature uses Node's `fs.watch()` API, so it is subject to
2346-     * the limitations described [here](https://nodejs.org/docs/latest/api/fs.html#fs_fs_watch_filename_options_listener).
2347-     */
2348-    autoUpdate?: boolean;
2349-    /**
2350-     * Configures a logger for warnings and errors. This can be a custom logger or an instance of.
2351-     * By default, it uses the same logger as the rest of the SDK.
2352-     */
2353-    logger?: LDLogger;
2354-    /**
2355-     * The SDK can support yaml if provided with a parser. The parser must output
2356-     * objects which are equivalent to the standard JSON parser. The parser of the
2357-     * `yaml` package can be used.
2358-     */
2359-    yamlParser?: (data: string) => any;
2360-}
2361-
2362-interface ClientSideAvailability {
2363-    usingMobileKey?: boolean;
2364-    usingEnvironmentId?: boolean;
2365-}
2366-
2367-type Op = 'in' | 'startsWith' | 'endsWith' | 'contains' | 'matches' | 'lessThan' | 'lessThanOrEqual' | 'greaterThan' | 'greaterThanOrEqual' | 'before' | 'after' | 'segmentMatch' | 'semVerEqual' | 'semVerGreaterThan' | 'semVerLessThan';
2368-interface Clause {
2369-    attribute: string;
2370-    negate?: boolean;
2371-    op: Op;
2372-    values: any[];
2373-    contextKind?: string;
2374-    attributeReference: AttributeReference;
2375-}
2376-
2377-interface WeightedVariation {
2378-    variation: number;
Thinking…

Good. Now write the code.

src/client.js:

const { init, basicLogger } = require('@launchdarkly/node-server-sdk');
const { TestData, FileDataSourceFactory } = require('@launchdarkly/node-server-sdk/integrations');

Note: exports map subpath "./integrations" → ok in CJS require.

createClient(mode):

  • if LD_SDK_KEY set → real client with streaming, waitForInitialization with timeout; if it fails, log.
  • else → offline/testdata.

Demo in index.js prints evaluations for a few contexts, variationDetail, then flush + close.

Let me write it and run.

For real mode, LD client's init starts connecting; without valid key it logs errors and never becomes ready — so handle with timeout fallback: await Promise.race([client.waitForInitialization(), sleep(3000)]).

app.js: checkoutExperience(client, context) returns object describing UI: new checkout enabled, banner message string from a multivariate flag, plus a config object flag.

Write files now.

File edits

1 file

Completed
Writtenlaunchdarkly-example/src/client.jsView changes

/sandbox/repo/launchdarkly-example/src/client.js

Written content · previous contents were not recorded

'use strict';
/**
* LaunchDarkly client factory.
*
* Three modes, chosen automatically:
*
* 1. "live" - LD_SDK_KEY is set. Talks to the hosted LaunchDarkly service over
* its streaming connection. This is what you ship to production.
* 2. "testdata" - No SDK key. Flags are defined in-process with TestData, so the
* demo runs offline and deterministically (no network, no service).
* 3. "file" - LD_FLAG_FILE=1 (and no SDK key). Flags come from flags/flags.json.
* Handy for CI and local development.
*
* Nothing here starts a long-running process: the demo in src/index.js closes the
* client and exits. In a real server you would keep the client alive for the
* lifetime of the process.
*/
const { init, basicLogger } = require('@launchdarkly/node-server-sdk');
const {
TestData,
FileDataSourceFactory,
} = require('@launchdarkly/node-server-sdk/integrations');
const path = require('node:path');
const FLAGS = {
newCheckout: 'show-new-checkout', // boolean
banner: 'promo-banner', // multivariate (string)
checkoutConfig: 'checkout-config', // JSON
};
/** Flag definitions used when there is no SDK key (offline / demo / CI). */
function buildTestData() {
const td = TestData.dataSource();
// Boolean rollout: on for beta users and for the "gold" plan, off for everyone else.
td.update(
td
.flag(FLAGS.newCheckout)
.booleanFlag()
.on(true)
.offVariation(false)
.fallthroughVariation(false)
.ifMatch('user', 'beta-tester', true)
.thenReturn(true)
.ifMatch('user', 'plan', 'gold', 'platinum')
.thenReturn(true),
);
// Multivariate: which promo banner copy to render.
td.update(
td
.flag(FLAGS.banner)
.variations('none', 'free-shipping', 'spring-sale')
.on(true)
.fallthroughVariation('none')
.ifMatch('user', 'country', 'US', 'CA')
.thenReturn('free-shipping')
.ifMatch('user', 'country', 'GB')
.thenReturn('spring-sale'),
);
// JSON: remote config that can change without a deploy.
td.update(
td
.flag(FLAGS.checkoutConfig)
.variations(
{ retries: 2, expressPay: false, timeoutMs: 5000 },
{ retries: 5, expressPay: true, timeoutMs: 2500 },
)
.on(true)
.fallthroughVariation(0)
.ifMatch('user', 'plan', 'gold', 'platinum')
.thenReturn(1),
);
return td;
}
function modeFromEnv() {
if (process.env.LD_SDK_KEY) return 'live';
if (process.env.LD_FLAG_FILE) return 'file';
return 'testdata';
}
/**
* @returns {{ client: import('@launchdarkly/node-server-sdk').LDClient, mode: string, ready: boolean }}
*/
async function createClient({ initTimeoutMs = 5000 } = {}) {
const mode = modeFromEnv();
const logger = basicLogger({ level: process.env.LD_LOG_LEVEL || 'warn' });
let options = { logger };
if (mode === 'testdata') {
// SDK key is not used for evaluation in this mode; a placeholder is required.
options = { ...options, updateProcessor: buildTestData(), sendEvents: false };
} else if (mode === 'file') {
const flagsFile = path.join(__dirname, '..', 'flags', 'flags.json');
options = {
...options,
updateProcessor: new FileDataSourceFactory({ paths: [flagsFile] }).getFactory(),
sendEvents: false,
};
}
const client = init(process.env.LD_SDK_KEY || 'offline-sdk-key', options);
let ready = false;
try {
await withTimeout(client.waitForInitialization(), initTimeoutMs);
ready = true;
} catch (err) {
// Never fatal: the SDK serves the default value you pass to variation().
logger.warn(`LaunchDarkly did not become ready (${err.message}); falling back to defaults.`);
}
return { client, mode, ready };
}
function withTimeout(promise, ms) {
return new Promise((resolve, reject) => {
const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
promise.then(
(v) => {
clearTimeout(timer);
resolve(v);
},
(e) => {
clearTimeout(timer);
reject(e);
},
);
});
}
module.exports = { createClient, FLAGS };

File edits

1 file

Completed
Writtenlaunchdarkly-example/src/app.jsView changes

/sandbox/repo/launchdarkly-example/src/app.js

Written content · previous contents were not recorded

'use strict';
const { FLAGS } = require('./client');
/**
* The application logic. It knows nothing about LaunchDarkly internals: it just
* asks for a value and always supplies a safe default, so an outage or a missing
* flag degrades to known-good behaviour instead of throwing.
*
* @param {import('@launchdarkly/node-server-sdk').LDClient} client
* @param {object} context a LaunchDarkly context, e.g. { kind: 'user', key: 'u-1' }
*/
async function renderCheckout(client, context) {
const newCheckout = await client.variation(FLAGS.newCheckout, context, false);
const banner = await client.variation(FLAGS.banner, context, 'none');
const config = await client.variation(FLAGS.checkoutConfig, context, {
retries: 2,
expressPay: false,
timeoutMs: 5000,
});
return {
page: newCheckout ? 'checkout-v2' : 'checkout-v1',
banner,
expressPay: config.expressPay,
timeoutMs: config.timeoutMs,
};
}
/**
* Same evaluation, but with LaunchDarkly's explanation of *why* the value came
* back the way it did. Useful in logs and when debugging targeting rules.
*/
async function explainCheckoutFlag(client, context) {
return client.variationDetail(FLAGS.newCheckout, context, false);
}
module.exports = { renderCheckout, explainCheckoutFlag };
Thinking…

Now index.js demo and flags.json and manage-flag.js script.

Note: td.flag(...).ifMatch(...).thenReturn(...) — after thenReturn it returns the flag builder (BuilderType = TestDataFlagBuilder), so chaining multiple ifMatch works. Good.

flags.json: use "flags" with full definitions matching Flag interface, or "flagValues". I'll use "flags" for the boolean with contextTargets to demonstrate. Simpler: flagValues for all three (flat values). But then no targeting. Let me do a mix: use flags for show-new-checkout with a rule, and flagValues for the others. Verify file data source accepts both keys — in js-core FileDataSource, it processes data.flags and data.flagValues. Let me check quickly in the bundled js.

cd /sandbox/repo/launchdarkly-example && grep -o "flagValues" node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -3 && grep -n "flagValues" -B 5 -A 10 node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -40
flagValues
flagValues
flagValues
flagValues
1088-  _processParsedData(parsed, oldData) {
1089-    Object.keys(parsed.flags || {}).forEach((key) => {
1090-      processFlag(parsed.flags[key]);
1091-      this._addItem(VersionedDataKinds.Features, parsed.flags[key]);
1092-    });
1093:    Object.keys(parsed.flagValues || {}).forEach((key) => {
1094-      var _a, _b;
1095-      const previousInstance = (_a = oldData[VersionedDataKinds.Features.namespace]) == null ? void 0 : _a[key];
1096-      let { version } = previousInstance != null ? previousInstance : { version: 1 };
1097:      if (previousInstance && JSON.stringify(parsed.flagValues[key]) !== JSON.stringify((_b = previousInstance == null ? void 0 : previousInstance.variations) == null ? void 0 : _b[0])) {
1098-        version += 1;
1099-      }
1100:      const flag = makeFlagWithValue(key, parsed.flagValues[key], version);
1101-      processFlag(flag);
1102-      this._addItem(VersionedDataKinds.Features, flag);
1103-    });
1104-    Object.keys(parsed.segments || {}).forEach((key) => {
1105-      processSegment(parsed.segments[key]);
1106-      this._addItem(VersionedDataKinds.Segments, parsed.segments[key]);
1107-    });
1108-  }
1109-};
1110-
--
1197-          }
1198-        } else {
1199-          parsed = JSON.parse(curr.data);
1200-        }
1201-        const flagsFromValues = {};
1202:        Object.entries((_a = parsed.flagValues) != null ? _a : {}).forEach(([key, value]) => {
1203-          flagsFromValues[key] = makeFlagWithValue(key, value, 1);
1204-        });
1205-        return {
1206-          segments: __spreadValues(__spreadValues({}, acc.segments), (_b = parsed.segments) != null ? _b : {}),
1207-          flags: __spreadValues(__spreadValues(__spreadValues({}, acc.flags), (_c = parsed.flags) != null ? _c : {}), flagsFromValues)
1208-        };
1209-      },
1210-      {
1211-        segments: {},
1212-        flags: {}
Thinking…

Good. Now write flags.json with a full flag definition for show-new-checkout (with rule) and flagValues for the others. Note in the loader, processFlag probably adds key/version. Let me write:

{
  "flags": {
    "show-new-checkout": {
      "key": "show-new-checkout",
      "version": 1,
      "on": true,
      "variations": [true, false],
      "offVariation": 1,
      "fallthrough": { "variation": 1 },
      "contextTargets": [{ "contextKind": "user", "values": ["beta-user-1"], "variation": 0 }],
      "rules": [ ... ]
    }
  },
  "flagValues": { "promo-banner": "free-shipping", "checkout-config": {...} }
}

Rule shape: { id, clauses: [{ attribute, op, values, contextKind, attributeReference }], variation or rollout }. attributeReference is required by processFlag maybe (AttributeReference created). Let me check processFlag to see whether it needs attributeReference or it will create it.

grep processFlag.

cd /sandbox/repo/launchdarkly-example && grep -n "const processFlag" -A 25 node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -40
(no output)
(no output)
Thinking…

Continue: find processFlag definition differently.

cd /sandbox/repo/launchdarkly-example && grep -n "processFlag" node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head && grep -n "processClause\|function processFlag\|processFlag =" -A 20 node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -60
519:function processFlag(flag) {
519:function processFlag(flag) {
594:    processFlag(flag);
623:    processFlag(parsed.data);
651:  processFlag(parsed);
1090:      processFlag(parsed.flags[key]);
1101:      processFlag(flag);
1134:          processFlag(flag);
1292:              processFlag(flag);
2824:                processFlag(flag);
3243:          processFlag(flag);
519:function processFlag(flag) {
520-  var _a;
521-  nullReplacer(flag, ["variations"]);
522-  if (flag.fallthrough && flag.fallthrough.rollout) {
523-    const rollout = flag.fallthrough.rollout;
524-    processRollout(rollout);
525-  }
526-  (_a = flag == null ? void 0 : flag.rules) == null ? void 0 : _a.forEach((rule) => {
527-    var _a2;
528-    processRollout(rule.rollout);
529-    (_a2 = rule == null ? void 0 : rule.clauses) == null ? void 0 : _a2.forEach((clause) => {
530-      if (clause && clause.attribute) {
531-        clause.attributeReference = new AttributeReference(clause.attribute, !clause.contextKind);
532-      } else if (clause) {
533-        clause.attributeReference = AttributeReference.InvalidReference;
534-      }
535-    });
536-  });
537-}
538-function processSegment(segment) {
539-  var _a, _b, _c, _d, _e;
Thinking…

Good — attributeReference is generated. Now write flags.json, index.js, scripts/manage-flag.js, README, .env.example, package.json scripts.

For the REST API script: LaunchDarkly Flags API: POST https://app.launchdarkly.com/api/v2/flags/{projectKey}/{environmentKey} with body { name, key, variations... }. PATCH with JSON Patch for targeting/rollout: [{ "op":"replace", "path":"/fallthrough/variation", "value": 0 }]. Requires header LD-API-Version: 1 and Authorization: <token>. Also PATCH needs Content-Type: application/json.

Also GET flag. And rollout: {"op":"replace","path":"/fallthrough","value":{"rollout":{"variations":[{"variation":0,"weight":25000},{"variation":1,"weight":75000}]}}} — weights in hundredths of a percent (100000 = 100%).

I'll write the script with commands: create, get, rollout <percent>, on/off (targeting), list. Use fetch (Node 22 has global fetch). No long-running.

Also verify script behaves gracefully without token (exit with message). And I can't test against real API without credentials — I'll add a --dry-run that prints the request it would make, and test that.

Let me write these files.

File edits

1 file

Completed
Writtenlaunchdarkly-example/flags/flags.jsonView changes

/sandbox/repo/launchdarkly-example/flags/flags.json

Written content · previous contents were not recorded

{
"flags": {
"show-new-checkout": {
"key": "show-new-checkout",
"version": 1,
"on": true,
"variations": [true, false],
"offVariation": 1,
"fallthrough": { "variation": 1 },
"contextTargets": [
{ "contextKind": "user", "values": ["beta-user-1", "beta-user-2"], "variation": 0 }
],
"rules": [
{
"id": "gold-plan",
"clauses": [
{
"contextKind": "user",
"attribute": "plan",
"op": "in",
"values": ["gold", "platinum"],
"negate": false
}
],
"variation": 0
}
]
}
},
"flagValues": {
"promo-banner": "free-shipping",
"checkout-config": { "retries": 5, "expressPay": true, "timeoutMs": 2500 }
}
}

File edits

1 file

Completed
Writtenlaunchdarkly-example/src/index.jsView changes

/sandbox/repo/launchdarkly-example/src/index.js

Written content · previous contents were not recorded

'use strict';
/**
* Runnable demo:
*
* node src/index.js # offline, uses TestData (no credentials needed)
* LD_FLAG_FILE=1 node src/index.js # offline, uses flags/flags.json
* LD_SDK_KEY=sdk-... node src/index.js # live against the hosted LaunchDarkly service
*
* Exits on its own. No local services, no long-running processes.
*/
const { createClient, FLAGS } = require('./client');
const { renderCheckout, explainCheckoutFlag } = require('./app');
const CONTEXTS = [
{ kind: 'user', key: 'u-anonymous' },
{ kind: 'user', key: 'u-beta', betaTester: true },
{ kind: 'user', key: 'u-gold', plan: 'gold', country: 'US' },
{ kind: 'user', key: 'u-uk', plan: 'silver', country: 'GB' },
// Multi-kind context: a user acting inside an organisation.
{
kind: 'multi',
user: { key: 'u-multi', plan: 'silver' },
org: { key: 'org-acme', betaTester: true },
},
];
async function main() {
const { client, mode, ready } = await createClient();
console.log(`LaunchDarkly mode: ${mode} (ready: ${ready})\n`);
console.log('Checkout rendering per context:');
for (const context of CONTEXTS) {
const view = await renderCheckout(client, context);
const label = context.kind === 'multi' ? context.user.key : context.key;
console.log(` ${label.padEnd(12)} -> ${JSON.stringify(view)}`);
}
console.log('\nWhy did show-new-checkout evaluate that way?');
for (const context of [CONTEXTS[0], CONTEXTS[2]]) {
const detail = await explainCheckoutFlag(client, context);
const label = context.key;
console.log(
` ${label.padEnd(12)} -> value=${detail.value} reason=${JSON.stringify(detail.reason)}`,
);
}
// Live changes: react to a flag update without redeploying.
client.on(`update:${FLAGS.newCheckout}`, (key, value) => {
console.log(`\nFlag "${key}" changed to ${value}`);
});
// In production you would keep the client open. Here we flush events and exit.
await client.flush();
await client.close();
console.log('\nDone. Client closed.');
}
main().catch((err) => {
console.error(err);
process.exit(1);
});
Thinking…

Now the manage-flag.js script and test it with --dry-run.

API details:

  • Base: https://app.launchdarkly.com/api/v2
  • Create flag: POST /flags/{projectKey}/{environmentKey} body {name, key, description?, variations?} → 201
  • Get flag: GET /flags/{projectKey}/{flagKey}?env={env}
  • List flags: GET /flags/{projectKey}
  • Update: PATCH /flags/{projectKey}/{flagKey} with JSON Patch, header LD-API-Version: 1
    • turn targeting on/off: [{"op":"replace","path":"/environments/{env}/on","value":true}]
    • rollout: [{"op":"replace","path":"/environments/{env}/fallthrough","value":{"rollout":{"variations":[{"variation":0,"weight":X},{"variation":1,"weight":100000-X}]}}}]
    • serve off variation to everyone: [{"op":"replace","path":"/environments/{env}/fallthrough","value":{"variation":1}}]

Write it with commands and dry-run support.

File edits

1 file

Completed
Writtenlaunchdarkly-example/scripts/manage-flag.jsView changes

/sandbox/repo/launchdarkly-example/scripts/manage-flag.js

Written content · previous contents were not recorded

#!/usr/bin/env node
'use strict';
/**
* Manage flags through the LaunchDarkly REST API (no CLI install, no local stack).
*
* Requires:
* LD_ACCESS_TOKEN personal access token (Account settings -> Authorization)
* LD_PROJECT_KEY defaults to "default"
* LD_ENV_KEY defaults to "test"
*
* Usage:
* node scripts/manage-flag.js list
* node scripts/manage-flag.js create show-new-checkout --name "Show new checkout"
* node scripts/manage-flag.js get show-new-checkout
* node scripts/manage-flag.js rollout show-new-checkout 25 # 25% of traffic
* node scripts/manage-flag.js on show-new-checkout # targeting on, 100%
* node scripts/manage-flag.js off show-new-checkout # kill switch
*
* Add --dry-run to print the HTTP request instead of sending it.
*/
const BASE = process.env.LD_API_BASE || 'https://app.launchdarkly.com/api/v2';
const PROJECT = process.env.LD_PROJECT_KEY || 'default';
const ENV = process.env.LD_ENV_KEY || 'test';
const TOKEN = process.env.LD_ACCESS_TOKEN;
function fail(msg) {
console.error(msg);
process.exit(1);
}
async function send(method, path, body, dryRun) {
const url = `${BASE}${path}`;
const headers = { 'LD-API-Version': '1' };
if (TOKEN) headers.Authorization = TOKEN;
if (body) headers['Content-Type'] = 'application/json';
if (dryRun) {
console.log(`${method} ${url}`);
if (body) console.log(JSON.stringify(body, null, 2));
return { dryRun: true, method, url, body };
}
if (!TOKEN) {
fail(
'LD_ACCESS_TOKEN is not set. Create one in LaunchDarkly (Account settings -> Authorization),\n' +
'or re-run with --dry-run to see the request that would be sent.',
);
}
const res = await fetch(url, { method, headers, body: body ? JSON.stringify(body) : undefined });
const text = await res.text();
if (!res.ok) {
fail(`${method} ${url} -> HTTP ${res.status}\n${text}`);
}
try {
return text ? JSON.parse(text) : { status: res.status };
} catch {
return text;
}
}
// JSON Patch helpers. Weights are in hundredths of a percent (100000 == 100%).
const patch = (ops) => ops;
const targetingPatch = (on) =>
patch([{ op: 'replace', path: `/environments/${ENV}/on`, value: on }]);
const rolloutPatch = (percent) => {
const weight = Math.round(percent * 1000);
return patch([
{ op: 'replace', path: `/environments/${ENV}/on`, value: true },
{
op: 'replace',
path: `/environments/${ENV}/fallthrough`,
value: {
rollout: {
variations: [
{ variation: 0, weight },
{ variation: 1, weight: 100000 - weight },
],
},
},
},
]);
};
async function main() {
const [, , command, arg, ...rest] = process.argv;
const dryRun = rest.includes('--dry-run') || process.argv.includes('--dry-run');
switch (command) {
case 'list':
return send('GET', `/flags/${PROJECT}?env=${ENV}`, null, dryRun);
case 'get': {
if (!arg) fail('Usage: get <flagKey>');
return send('GET', `/flags/${PROJECT}/${arg}?env=${ENV}`, null, dryRun);
}
case 'create': {
if (!arg) fail('Usage: create <flagKey> [--name "Human name"]');
const i = rest.indexOf('--name');
const name = i >= 0 ? rest[i + 1] : arg;
return send(
'POST',
`/flags/${PROJECT}/${ENV}`,
{ name, key: arg, variations: [{ value: true }, { value: false }] },
dryRun,
);
}
case 'on':
if (!arg) fail('Usage: on <flagKey>');
return send('PATCH', `/flags/${PROJECT}/${arg}`, targetingPatch(true), dryRun);
case 'off':
if (!arg) fail('Usage: off <flagKey>');
return send('PATCH', `/flags/${PROJECT}/${arg}`, targetingPatch(false), dryRun);
case 'rollout': {
if (!arg) fail('Usage: rollout <flagKey> <percent>');
const percent = Number(rest[0]);
if (!Number.isFinite(percent) || percent < 0 || percent > 100) {
fail('percent must be a number between 0 and 100');
}
return send('PATCH', `/flags/${PROJECT}/${arg}`, rolloutPatch(percent), dryRun);
}
default:
fail('Unknown command. See the header of this file for usage.');
}
}
main()
.then((result) => {
if (!result.dryRun) console.log(JSON.stringify(result, null, 2).slice(0, 2000));
})
.catch((err) => fail(err.message));
Thinking…

Now test: run index.js in testdata mode, file mode, and dry-run the script.

cd /sandbox/repo/launchdarkly-example && timeout 60 node src/index.js; echo "EXIT:$?"
TypeError: TestData.dataSource is not a function
TypeError: TestData.dataSource is not a function
    at buildTestData (/sandbox/repo/launchdarkly-example/src/client.js:35:23)
    at createClient (/sandbox/repo/launchdarkly-example/src/client.js:97:46)
    at main (/sandbox/repo/launchdarkly-example/src/index.js:30:41)
    at Object.<anonymous> (/sandbox/repo/launchdarkly-example/src/index.js:60:1)
    at Module._compile (node:internal/modules/cjs/loader:1781:14)
    at Object..js (node:internal/modules/cjs/loader:1913:10)
    at Module.load (node:internal/modules/cjs/loader:1505:32)
    at Function._load (node:internal/modules/cjs/loader:1309:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
EXIT:1
Thinking…

TestData is exported as a class with static dataSource()? In js-core, TestData.dataSource() is a static method... Let me check the export shape.

cd /sandbox/repo/launchdarkly-example && node -e "const i=require('@launchdarkly/node-server-sdk/integrations');console.log(Object.keys(i));console.log(typeof i.TestData, typeof i.FileDataSourceFactory);" && grep -n "declare class TestData" -A 30 node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts | head -50
[
[
  'FileDataSourceFactory',
  'TestData',
  'TestDataFlagBuilder',
  'TestDataRuleBuilder'
]
function function
2475:declare class TestDataRuleBuilder<BuilderType> {
2476-    private readonly _flagBuilder;
2477-    private _clauses;
2478-    private _variation?;
2479-    /**
2480-     * Adds another clause using the "is one of" operator.
2481-     *
2482-     * For example, this creates a rule that returns `true` if the name is
2483-     * "Patsy" and the country is "gb":
2484-     *
2485-     *     testData.flag('flag')
2486-     *             .ifMatch('name', 'Patsy')
2487-     *             .andMatch('country', 'gb')
2488-     *             .thenReturn(true)
2489-     *
2490-     * @param contextKind the kind of the context
2491-     * @param attribute the user attribute to match against
2492-     * @param values values to compare to
2493-     * @return the flag rule builder
2494-     */
2495-    andMatch(contextKind: string, attribute: string, ...values: any): TestDataRuleBuilder<BuilderType>;
2496-    /**
2497-     * Adds another clause using the "is not one of" operator.
2498-     *
2499-     * For example, this creates a rule that returns `true` if the name is
2500-     * "Patsy" and the country is not "gb":
2501-     *
2502-     *     testData.flag('flag')
2503-     *             .ifMatch('name', 'Patsy')
2504-     *             .andNotMatch('country', 'gb')
2505-     *             .thenReturn(true)
--
2533:declare class TestDataFlagBuilder {
2534-    private readonly _key;
2535-    private _data;
2536-    private get _isBooleanFlag();
2537-    /**
2538-     * A shortcut for setting the flag to use the standard boolean configuration.
2539-     *
2540-     * This is the default for all new flags created with {@link TestData.flag}. The
2541-     * flag will have two variations, `true` and `false` (in that order). It
2542-     * will return `false` whenever targeting is off and `true` when targeting
2543-     * is on unless other settings specify otherwise.
2544-     *
2545-     * @return the flag builder
2546-     */
2547-    booleanFlag(): TestDataFlagBuilder;
2548-    /**
2549-     * Sets the allowable variation values for the flag.
2550-     *
Thinking…

TestData is exported as a function — maybe it's the class itself and dataSource is a static; typeof function works for classes. But error says dataSource is not a function. Maybe the export is a factory function TestData() returning instance? Let's check the TestData class declaration in d.ts (search "declare class TestData<" or "declare class TestData ").

cd /sandbox/repo/launchdarkly-example && grep -n "TestData" node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts | grep -v RuleBuilder | grep -v FlagBuilder | head -20
1742: * TestData or FileData data sources, nor will it be applied to any data source provided through the featureStore
1742:     * TestData or FileData data sources, nor will it be applied to any data source provided through the featureStore
2531: * A builder for feature flag configurations to be used with {@link TestData}.
2540:     * This is the default for all new flags created with {@link TestData.flag}. The
2564:     * that you get from calling {@link TestData.flag} with a new flag key, the flag
2726: * data that the application has put into it using the {@link TestData.update} method.
2729: *     // Import TestData from the integrations package of the SDK you are using.
2732: *     const td = TestData();
2750: * If the same `TestData` instance is used to configure multiple `LDClient` instances,
2753:declare class TestData {
2759:     * Get a factory for update processors that will be attached to this TestData instance.
2766:     * If the flag key has already been defined in this `TestData` instance,
2777:     * {@link TestData.update}.
2790:     * this `TestData`. If no `LDClient` has been started yet, it simply adds
2805:     * configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds this
2824:     * configured to use this `TestData`. If no `LDClient` has been started yet, it simply adds
2846:type index$1_TestData = TestData;
2847:declare const index$1_TestData: typeof TestData;
Thinking…

So it's TestData() — a function call, not TestData.dataSource(). Also updateProcessor: td.getFactory()? Docs in js-core: updateProcessor: td.getFactory(). Let me check lines 2753-2800.

cd /sandbox/repo/launchdarkly-example && sed -n 2725,2770p node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts
* Unlike `FileData`, this mechanism does not use any external resources. It provides only the
 * Unlike `FileData`, this mechanism does not use any external resources. It provides only the
 * data that the application has put into it using the {@link TestData.update} method.
 *
 * ```
 *     // Import TestData from the integrations package of the SDK you are using.
 *     // This is a common implementation and may be used in multiple SDKs.
 *
 *     const td = TestData();
 *     testData.update(td.flag("flag-key-1").booleanFlag().variationForAll(true));
 *     // Use the initialization mechanism specified by your SDK.
 *     const client = LDClient.init(sdkKey, { updateProcessor: td.getFactory() });
 *
 *     // flags can be updated at any time:
 *     td.update(td.flag("flag-key-2")
 *         .variationForContext("user", "some-user-key", true)
 *         .fallthroughVariation(false));
 * ```
 *
 * The above example uses a simple boolean flag, but more complex configurations are possible using
 * the methods of the {@link TestDataFlagBuilder} that is returned by {@link TestData.flag}.
 * {@link TestDataFlagBuilder} supports many of the ways a flag can be configured on the
 * LaunchDarkly dashboard, but does not currently support
 *  1. rule operators other than "in" and "not in", or
 *  2. percentage rollouts.
 *
 * If the same `TestData` instance is used to configure multiple `LDClient` instances,
 * any changes made to the data will propagate to all of the `LDClient`s.
 */
declare class TestData {
    private _currentFlags;
    private _currentSegments;
    private _dataSources;
    private _flagBuilders;
    /**
     * Get a factory for update processors that will be attached to this TestData instance.
     * @returns An update processor factory.
     */
    getFactory(): (clientContext: LDClientContext, featureStore: LDFeatureStore, initSuccessHandler: VoidFunction, errorHandler?: (e: Error) => void) => subsystem.LDStreamProcessor;
    /**
     * Creates or copies a {@link TestDataFlagBuilder} for building a test flag configuration.
     *
     * If the flag key has already been defined in this `TestData` instance,
     * then the builder starts with the same configuration that was last
     * provided for this flag.
     *
     * Otherwise, it starts with a new default configuration in which the flag
Thinking…

Fix client.js: const td = TestData(); ... updateProcessor: td.getFactory(). Also keep td reference so we could update live — expose it maybe for demo of live change. I'll return the td instance from buildTestData and use it in index.js to demonstrate a live flag change (push an update after evaluations). But client.close() ends quickly; I can do: after printing, update flag via td and wait ~200ms to see the update: event, then close. That's nice and short.

Let me restructure client.js to export createClient returning {client, mode, ready, testData}.

Also for the demo of live change, note in live mode you'd change it in the dashboard.

Let me apply edits.

File edits

1 file

Completed
Modifiedlaunchdarkly-example/src/client.js+5−3View changes

/sandbox/repo/launchdarkly-example/src/client.js

Replacement excerpt

/** Flag definitions used when there is no SDK key (offline / demo / CI). */
function buildTestData() {
- const td = TestData.dataSource();
+ const td = TestData();

Replacement excerpt

let options = { logger };
+ let testData = null;
if (mode === 'testdata') {
// SDK key is not used for evaluation in this mode; a placeholder is required.
- options = { ...options, updateProcessor: buildTestData(), sendEvents: false };
+ testData = buildTestData();
+ options = { ...options, updateProcessor: testData.getFactory(), sendEvents: false };
} else if (mode === 'file') {

Replacement excerpt

- return { client, mode, ready };
+ return { client, mode, ready, testData };
}
Thinking…

Now update index.js to use testData for the live-change demo.

File edits

1 file

Completed
Modifiedlaunchdarkly-example/src/index.js+12−2View changes

/sandbox/repo/launchdarkly-example/src/index.js

Replacement excerpt

- const { client, mode, ready } = await createClient();
+ const { client, mode, ready, testData } = await createClient();

Replacement excerpt

// Live changes: react to a flag update without redeploying.
client.on(`update:${FLAGS.newCheckout}`, (key, value) => {
- console.log(`\nFlag "${key}" changed to ${value}`);
+ console.log(`\nFlag "${key}" changed to ${value} (no redeploy needed)`);
});
+
+ if (testData) {
+ // Simulate flipping the flag in the LaunchDarkly dashboard.
+ testData.update(testData.flag(FLAGS.newCheckout).booleanFlag().variationForAll(true));
+ await new Promise((resolve) => setTimeout(resolve, 250));
+ console.log('After the change:');
+ for (const context of [CONTEXTS[0], CONTEXTS[3]]) {
+ console.log(` ${context.key.padEnd(12)} -> ${JSON.stringify(await renderCheckout(client, context))}`);
+ }
+ }
Thinking…

Run again.

cd /sandbox/repo/launchdarkly-example && timeout 60 node src/index.js; echo "EXIT:$?"
TypeError: Class constructor TestData cannot be invoked without 'new'
TypeError: Class constructor TestData cannot be invoked without 'new'
    at buildTestData (/sandbox/repo/launchdarkly-example/src/client.js:35:14)
    at createClient (/sandbox/repo/launchdarkly-example/src/client.js:98:16)
    at main (/sandbox/repo/launchdarkly-example/src/index.js:30:51)
    at Object.<anonymous> (/sandbox/repo/launchdarkly-example/src/index.js:70:1)
    at Module._compile (node:internal/modules/cjs/loader:1781:14)
    at Object..js (node:internal/modules/cjs/loader:1913:10)
    at Module.load (node:internal/modules/cjs/loader:1505:32)
    at Function._load (node:internal/modules/cjs/loader:1309:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
EXIT:1
Thinking…

So new TestData(). Docs example says TestData() for js-core v8? In this bundled version it's a class requiring new. Let me check whether static dataSource exists on the class prototype... typeof i.TestData === 'function' and it's a class. Check for static method: Object.getOwnPropertyNames(TestData).

cd /sandbox/repo/launchdarkly-example && node -e "const {TestData}=require('@launchdarkly/node-server-sdk/integrations');console.log(Object.getOwnPropertyNames(TestData));console.log(Object.getOwnPropertyNames(TestData.prototype));"
[ 'length', 'name', 'prototype' ]
[ 'length', 'name', 'prototype' ]
[
  'constructor',
  'getFactory',
  'flag',
  'update',
  'usePreconfiguredFlag',
  'usePreconfiguredSegment'
]
Thinking…

I need to instantiate a new TestData object.

File edits

1 file

Completed
Modifiedlaunchdarkly-example/src/client.js+1−1View changes

/sandbox/repo/launchdarkly-example/src/client.js

Replacement excerpt

- const td = TestData();
+ const td = new TestData();
Thinking…

Run again.

cd /sandbox/repo/launchdarkly-example && timeout 60 node src/index.js; echo "EXIT:$?"
The waitForInitialization function was called without a timeout specified. In a future version a default timeout will be applied.
The waitForInitialization function was called without a timeout specified. In a future version a default timeout will be applied.
LaunchDarkly mode: testdata (ready: true)

Checkout rendering per context:
  u-anonymous  -> {"page":"checkout-v1","banner":"none","expressPay":false,"timeoutMs":5000}
  u-beta       -> {"page":"checkout-v1","banner":"none","expressPay":false,"timeoutMs":5000}
  u-gold       -> {"page":"checkout-v2","banner":"none","expressPay":true,"timeoutMs":2500}
  u-uk         -> {"page":"checkout-v1","banner":"none","expressPay":false,"timeoutMs":5000}
  u-multi      -> {"page":"checkout-v1","banner":"none","expressPay":false,"timeoutMs":5000}

Why did show-new-checkout evaluate that way?
  u-anonymous  -> value=false reason={"kind":"FALLTHROUGH"}
  u-gold       -> value=true reason={"kind":"RULE_MATCH","ruleId":"rule1","ruleIndex":1}

Flag "[object Object]" changed to undefined (no redeploy needed)
After the change:
  u-anonymous  -> {"page":"checkout-v2","banner":"none","expressPay":false,"timeoutMs":5000}
  u-uk         -> {"page":"checkout-v2","banner":"none","expressPay":false,"timeoutMs":5000}

Done. Client closed.
EXIT:0
Thinking…

Several issues:

  1. Banner flag always "none" — targeting rules for banner didn't match: ifMatch('user','country','US','CA') — u-gold has country US, should return 'free-shipping'. But it returned 'none'. Also u-beta with betaTester true didn't get new checkout. Hmm, and ruleIndex 1 matched for u-gold — meaning rule 1 (index 1) matched, which is the plan rule (rules are 0-indexed: rule0 beta-tester, rule1 plan). Wait, why is rule1 index... Actually the beta rule should be rule 0. For u-beta, ifMatch('user','beta-tester', true) — attribute name 'beta-tester' but the context attribute is betaTester. That's my bug! TestData ifMatch uses the attribute reference string; 'beta-tester' maps to attributeReference for literal name "beta-tester", not camelCase betaTester. Fix: use betaTester.

  2. Banner country rule: u-gold country 'US' → should return 'free-shipping'. It returned 'none'. Why? Perhaps because with multivariate flags and thenReturn('free-shipping'), string variations work... hmm. Maybe rule index issue: fallthroughVariation('none')? Or maybe the variation matching failed because variations('none','free-shipping','spring-sale') plus thenReturn('free-shipping') looks up index by value — should be 1.

Wait — maybe the issue is that flags are keyed and my three flags... banner rule uses ifMatch('user','country','US','CA'). Should match. Unless the country attribute is filtered? No.

Hmm, maybe offVariation/on(true) etc fine. Let me debug directly: create td, build banner flag, evaluate with variationDetail to see reason.

  1. The update event payload: client.on('update:key', (key, value) => ...) gave "[object Object]" and undefined — so the callback receives (currentValue, oldValue)? Actually in js-core server SDK, the "update:key" event emits (currentValue, oldValue). And the general 'update' event emits (key, currentValue, oldValue). So fix: use update:${key} with (value, previous) or use 'update' event. Let me check the d.ts for LDClient 'on' signature.

  2. waitForInitialization timeout warning: pass a timeout to waitForInitialization(ms) — the SDK supports waitForInitialization(timeoutMs?) maybe. Let me check signature; if supported, use it instead of my race.

Let me check LDClient.d.ts.

cd /sandbox/repo/launchdarkly-example && cat node_modules/@launchdarkly/node-server-sdk/dist/src/api/LDClient.d.ts | head -80 && grep -n "waitForInitialization" -A 10 node_modules/@launchdarkly/js-server-sdk-common/dist/index.d.ts | head -40
/// <reference types="node" />
/// <reference types="node" />
import { EventEmitter } from 'events';
import { LDClient as LDClientCommon } from '@launchdarkly/js-server-sdk-common';
import { BigSegmentStoreStatusProvider } from './interfaces';
/**
 * The LaunchDarkly SDK client object.
 *
 * Create this object with {@link init}. Applications should configure the client at startup time
 * and continue to use it throughout the lifetime of the application, rather than creating instances
 * on the fly.
 *
 * Note that `LDClient` inherits from `EventEmitter`, so you can use the standard `on()`, `once()`, and
 * `off()` methods to receive events. The standard `EventEmitter` methods are not documented here; see the
 * {@link https://nodejs.org/api/events.html#events_class_eventemitter|Node API documentation}. For a
 * description of events you can listen for, see {@link on}.
 *
 */
export interface LDClient extends LDClientCommon, EventEmitter {
    /**
     * A mechanism for tracking the status of a Big Segment store.
     *
     * This object has methods for checking whether the Big Segment store is (as far as the SDK
     * knows) currently operational and tracking changes in this status. See
     * {@link interfaces.BigSegmentStoreStatusProvider} for more about this functionality.
     */
    readonly bigSegmentStoreStatusProvider: BigSegmentStoreStatusProvider;
    /**
     *
     * Registers an event listener that will be called when the client triggers some type of event.
     *
     * This is the standard `on` method inherited from Node's `EventEmitter`; see the
     * {@link https://nodejs.org/api/events.html#events_class_eventemitter|Node API docs} for more
     * details on how to manage event listeners. Here is a description of the event types defined
     * by `LDClient`.
     *
     * - `"ready"`: Sent only once, when the client has successfully connected to LaunchDarkly.
     * Alternately, you can detect this with [[waitForInitialization]].
     * - `"failed"`: Sent only once, if the client has permanently failed to connect to LaunchDarkly.
     * Alternately, you can detect this with [[waitForInitialization]].
     * - `"error"`: Contains an error object describing some abnormal condition that the client has detected
     * (such as a network error).
     * - `"update"`: The client has received a change to a feature flag. The event parameter is an object
     * containing a single property, `key`, the flag key. Note that this does not necessarily mean the flag's
     * value has changed for any particular context, only that some part of the flag configuration was changed.
     * - `"update:KEY"`: The client has received a change to the feature flag whose key is KEY. This is the
     * same as `"update"` but allows you to listen for a specific flag.
     *
     * @param event the name of the event to listen for
     * @param listener the function to call when the event happens
     */
    on(event: string | symbol, listener: (...args: any[]) => void): this;
}
//# sourceMappingURL=LDClient.d.ts.map1915: * Options for the waitForInitialization method.
1916- */
1917-interface LDWaitForInitializationOptions {
1918-    /**
1919-     * The amount of time, in seconds, to wait for initialization before rejecting the promise.
1920-     *
1921:     * If no options are specified on the `waitForInitialization`, then the promise will resolve
1922-     * only when initialization completes successfully or encounters a failure.
1923-     *
1924-     * Using a high timeout, or no timeout, is not recommended because it could result in a long
1925-     * delay when conditions prevent successful initialization.
1926-     *
1927-     * A value of 0 will cause the promise to resolve without waiting. In that scenario it would be
1928:     * more effective to not call `waitForInitialization`.
1929-     */
1930-    timeout: number;
1931-}
1932-
1933-/**
1934- * The LaunchDarkly SDK client object.
1935- *
1936- * Create this object with {@link init}. Applications should configure the client at startup time
1937- * and continue to use it throughout the lifetime of the application, rather than creating instances
1938- * on the fly.
--
1967:     * a specific call to waitForInitialization will wait before rejecting its promise. If a
1968:     * subsequent call is made to waitForInitialization with a timeout, then it will again wait up to
1969-     * that maximum time.
1970-     *
1971-     * Regardless of whether you continue to wait, the SDK will still retry all connection failures
1972-     * indefinitely unless it gets an unrecoverable error as described above.
1973-     *
1974-     * Waiting indefinitely, or depending only on the "ready" or "failed" events can result in an
1975-     * application waiting indefinitely. It is recommended to use a timeout which is reasonable
1976-     * for your application.
1977-     *
1978:     * @param options Options which control the behavior of `waitForInitialization`.
1979-     *
1980-     * @returns
1981-     * A Promise that will be resolved if the client initializes successfully, or rejected if it
Thinking…

Use client.waitForInitialization({ timeout: initTimeoutMs / 1000 }) (seconds). And drop my withTimeout helper (or keep as extra safety? simpler: use SDK timeout). I'll use the SDK's own option and remove the helper.

For the update event: "update" event param is an object containing key. For "update:KEY", the listener args? In js-core implementation, emitter emits 'update:KEY' with (currentValue, oldValue)? Let's check js implementation.

grep "update:" in index.js of common.

cd /sandbox/repo/launchdarkly-example && grep -n "update:" node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -10 && grep -n "emit(\"update\"\|emit('update'" -B 8 node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -30
(no output)
(no output)
Thinking…

Let me search differently: how emitter emits updates.

cd /sandbox/repo/launchdarkly-example && grep -n "\"update" node_modules/@launchdarkly/js-server-sdk-common/dist/index.js | head -20
(no output)
(no output)
Thinking…

Hmm no matches — maybe the emitter code is in node-server-sdk dist or the string is built via template update:${key}. grep for update with backtick.

cd /sandbox/repo/launchdarkly-example && grep -rn 'update' node_modules/@launchdarkly/node-server-sdk/dist/src/*.js | grep -i emit | head -10; grep -rn 'emit' node_modules/@launchdarkly/node-server-sdk/dist/src/index.js | head -10
node_modules/@launchdarkly/node-server-sdk/dist/src/LDClientNode.js:56: emitter.emit('update', { key });
node_modules/@launchdarkly/node-server-sdk/dist/src/LDClientNode.js:56:                emitter.emit('update', { key });
node_modules/@launchdarkly/node-server-sdk/dist/src/LDClientNode.js:57:                emitter.emit(`update:${key}`, { key });
Thinking…

So both 'update' and 'update:KEY' emit { key }. The observed output "[object Object]" and undefined matched: first arg {key} stringified, second undefined. So fix listener: (event) => console.log(...event.key). To get the value, call variation for a context.

Now also fix the banner/beta issues. Debug banner quickly.

Let me write a quick scratch test.

cd /sandbox/repo/launchdarkly-example && timeout 30 node -e "
const {init,basicLogger}=require('@launchdarkly/node-server-sdk');
const {TestData}=require('@launchdarkly/node-server-sdk/integrations');
const td=new TestData();
td.update(td.flag('promo-banner').variations('none','free-shipping','spring-sale').on(true).fallthroughVariation('none').ifMatch('user','country','US','CA').thenReturn('free-shipping').ifMatch('user','country','GB').thenReturn('spring-sale'));
const c=init('offline',{updateProcessor:td.getFactory(),sendEvents:false,logger:basicLogger({level:'error'})});
c.waitForInitialization({timeout:5}).then(async()=>{
 for (const ctx of [{kind:'user',key:'a',country:'US'},{kind:'user',key:'b',country:'GB'},{kind:'user',key:'c'}]) {
   console.log(ctx.country, JSON.stringify(await c.variationDetail('promo-banner',ctx,'none')));
 }
 await c.close();
});
"
US {"value":"none","variationIndex":null,"reason":{"kind":"ERROR","errorKind":"MALFORMED_FLAG"}}
US {"value":"none","variationIndex":null,"reason":{"kind":"ERROR","errorKind":"MALFORMED_FLAG"}}
GB {"value":"none","variationIndex":null,"reason":{"kind":"ERROR","errorKind":"MALFORMED_FLAG"}}
undefined {"value":"none","variationIndex":null,"reason":{"kind":"ERROR","errorKind":"MALFORMED_FLAG"}}
Thinking…

MALFORMED_FLAG — likely because fallthroughVariation('none') with string works but maybe on(true) + variations set makes boolean detection wrong... Or the issue: thenReturn('free-shipping') — the signature is thenReturn(variation: number | boolean)! So strings are not allowed; you must use the variation index. Indeed the d.ts said "either true or false or the index of the desired variation". Same for fallthroughVariation and offVariation: number | boolean.

So use indices: fallthroughVariation(0), thenReturn(1), thenReturn(2).

Let me verify.

cd /sandbox/repo/launchdarkly-example && timeout 30 node -e "
const {init,basicLogger}=require('@launchdarkly/node-server-sdk');
const {TestData}=require('@launchdarkly/node-server-sdk/integrations');
const td=new TestData();
td.update(td.flag('promo-banner').variations('none','free-shipping','spring-sale').on(true).fallthroughVariation(0).ifMatch('user','country','US','CA').thenReturn(1).ifMatch('user','country','GB').thenReturn(2));
const c=init('offline',{updateProcessor:td.getFactory(),sendEvents:false,logger:basicLogger({level:'error'})});
c.waitForInitialization({timeout:5}).then(async()=>{
 for (const ctx of [{kind:'user',key:'a',country:'US'},{kind:'user',key:'b',country:'GB'},{kind:'user',key:'c'}]) {
   console.log(ctx.country, JSON.stringify(await c.variationDetail('promo-banner',ctx,'none')));
 }
 await c.close();
});
"
US {"value":"free-shipping","variationIndex":1,"reason":{"kind":"RULE_MATCH","ruleId":"rule0","ruleIndex":0}}
US {"value":"free-shipping","variationIndex":1,"reason":{"kind":"RULE_MATCH","ruleId":"rule0","ruleIndex":0}}
GB {"value":"spring-sale","variationIndex":2,"reason":{"kind":"RULE_MATCH","ruleId":"rule1","ruleIndex":1}}
undefined {"value":"none","variationIndex":0,"reason":{"kind":"FALLTHROUGH"}}
Thinking…

Now update client.js: use indices for banner and checkoutConfig (checkoutConfig already used indices 0/1 — good), and fix beta attribute name to betaTester. Also update index.js event listener and waitForInitialization timeout usage.

Also in index.js the testData.update(...) live-change demo — fine.

Apply edits.

File edits

1 file

Completed
Modifiedlaunchdarkly-example/src/client.js+13−23View changes

/sandbox/repo/launchdarkly-example/src/client.js

Replacement excerpt

.fallthroughVariation(false)
- .ifMatch('user', 'beta-tester', true)
+ .ifMatch('user', 'betaTester', true)
.thenReturn(true)
.ifMatch('user', 'plan', 'gold', 'platinum')
.thenReturn(true),

Replacement excerpt

// Multivariate: which promo banner copy to render.
+ // Note: rules reference a variation by index, not by value.
td.update(
td
.flag(FLAGS.banner)
- .variations('none', 'free-shipping', 'spring-sale')
+ .variations('none', 'free-shipping', 'spring-sale') // 0, 1, 2
.on(true)
- .fallthroughVariation('none')
+ .fallthroughVariation(0)
.ifMatch('user', 'country', 'US', 'CA')
- .thenReturn('free-shipping')
+ .thenReturn(1)
.ifMatch('user', 'country', 'GB')
- .thenReturn('spring-sale'),
+ .thenReturn(2),
);

Replacement excerpt

.on(true)
.fallthroughVariation(0)
.ifMatch('user', 'plan', 'gold', 'platinum')
.thenReturn(1),
+ );
+
+ // Placeholder to keep the builder list tidy.
+ void td.update(

Replacement excerpt

let ready = false;
try {
- await withTimeout(client.waitForInitialization(), initTimeoutMs);
+ // timeout is in seconds; if LaunchDarkly is unreachable the SDK keeps serving
+ // the defaults you pass to variation(), so this is never fatal.
+ await client.waitForInitialization({ timeout: Math.ceil(initTimeoutMs / 1000) });
ready = true;
} catch (err) {
- // Never fatal: the SDK serves the default value you pass to variation().
logger.warn(`LaunchDarkly did not become ready (${err.message}); falling back to defaults.`);
}
return { client, mode, ready, testData };
-}
-
-function withTimeout(promise, ms) {
- return new Promise((resolve, reject) => {
- const timer = setTimeout(() => reject(new Error(`timed out after ${ms}ms`)), ms);
- promise.then(
- (v) => {
- clearTimeout(timer);
- resolve(v);
- },
- (e) => {
- clearTimeout(timer);
- reject(e);
- },
- );
- });
}
Thinking…

Oops, my third edit added a weird "void td.update(" placeholder hack that would break syntax (the checkoutConfig block already ended with );). Let me look at the file around that region and fix.

read /sandbox/repo/launchdarkly-example/src/client.js{"content":[{"text":" .flag(FLAGS.banner)\n .variations('none', 'free-shipping', 'spring-sale') // 0, 1, 2\n .on(true)\n .fallthroughVariati…ctrl+o
{"content":[{"text":"      .flag(FLAGS.banner)\n      .variations('none', 'free-shipping', 'spring-sale') // 0, 1, 2\n      .on(true)\n      .fallthroughVariation(0)\n      .ifMatch('user', 'country', 'US', 'CA')\n      .thenReturn(1)\n      .ifMatch('user', 'country', 'GB')\n      .thenReturn(2),\n  );\n\n  // JSON: remote config that can change without a deploy.\n  td.update(\n    td\n      .flag(FLAGS.checkoutConfig)\n      .variations(\n        { retries: 2, expressPay: false, timeoutMs: 5000 },\n        { retries: 5, expressPay: true, timeoutMs: 2500 },\n      )\n      .on(true)\n      .fallthroughVariation(0)\n      .ifMatch('user', 'plan', 'gold', 'platinum')\n      .thenReturn(1),\n  );\n\n  // Placeholder to keep the builder list tidy.\n  void td.update(\n  );\n\n  return td;\n}\n\nfunction modeFromEnv() {\n  if (process.env.LD_SDK_KEY) return 'live';\n  if (process.env.LD_FLAG_FILE) return 'file';\n  return 'testdata';\n}\n\n/**\n * @returns {{ client: import('@launchdarkly/node-server-sdk').LDClient, mode: string, ready: boolean }}\n */\n\n[36 more lines in file. Use offset=95 to continue.]","type":"text"}]}
Thinking…

Remove the placeholder block.

File edits

1 file

Completed
Modifiedlaunchdarkly-example/src/client.js+1−5View changes

/sandbox/repo/launchdarkly-example/src/client.js

Replacement excerpt

- // Placeholder to keep the builder list tidy.
- void td.update(
- );
-
return td;

Replacement excerpt

- * @returns {{ client: import('@launchdarkly/node-server-sdk').LDClient, mode: string, ready: boolean }}
+ * @returns {{ client: import('@launchdarkly/node-server-sdk').LDClient, mode: string, ready: boolean, testData: object|null }}
Thinking…

Now fix index.js event listener.

File edits

1 file

Completed
Modifiedlaunchdarkly-example/src/index.js+3−2View changes

/sandbox/repo/launchdarkly-example/src/index.js

Replacement excerpt

- client.on(`update:${FLAGS.newCheckout}`, (key, value) => {
- console.log(`\nFlag "${key}" changed to ${value} (no redeploy needed)`);
+ // The "update:KEY" event carries { key }; re-evaluate to see the new value.
+ client.on(`update:${FLAGS.newCheckout}`, ({ key }) => {
+ console.log(`\nFlag "${key}" changed in LaunchDarkly (no redeploy needed)`);
});
Thinking…

Run again, plus file mode and dry-run script.

cd /sandbox/repo/launchdarkly-example && timeout 60 node src/index.js; echo "EXIT:$?"; echo "=== FILE MODE ==="; LD_FLAG_FILE=1 timeout 60 node src/index.js; echo "EXIT:$?"
LaunchDarkly mode: testdata (ready: true)
LaunchDarkly mode: testdata (ready: true)

Checkout rendering per context:
  u-anonymous  -> {"page":"checkout-v1","banner":"none","expressPay":false,"timeoutMs":5000}
  u-beta       -> {"page":"checkout-v2","banner":"none","expressPay":false,"timeoutMs":5000}
  u-gold       -> {"page":"checkout-v2","banner":"free-shipping","expressPay":true,"timeoutMs":2500}
  u-uk         -> {"page":"checkout-v1","banner":"spring-sale","expressPay":false,"timeoutMs":5000}
  u-multi      -> {"page":"checkout-v1","banner":"none","expressPay":false,"timeoutMs":5000}

Why did show-new-checkout evaluate that way?
  u-anonymous  -> value=false reason={"kind":"FALLTHROUGH"}
  u-gold       -> value=true reason={"kind":"RULE_MATCH","ruleId":"rule1","ruleIndex":1}

Flag "show-new-checkout" changed in LaunchDarkly (no redeploy needed)
After the change:
  u-anonymous  -> {"page":"checkout-v2","banner":"none","expressPay":false,"timeoutMs":5000}
  u-uk         -> {"page":"checkout-v2","banner":"spring-sale","expressPay":false,"timeoutMs":5000}

Done. Client closed.
EXIT:0
=== FILE MODE ===
LaunchDarkly mode: file (ready: true)

Checkout rendering per context:
  u-anonymous  -> {"page":"checkout-v1","banner":"free-shipping","expressPay":true,"timeoutMs":2500}
  u-beta       -> {"page":"checkout-v1","banner":"free-shipping","expressPay":true,"timeoutMs":2500}
  u-gold       -> {"page":"checkout-v2","banner":"free-shipping","expressPay":true,"timeoutMs":2500}
  u-uk         -> {"page":"checkout-v1","banner":"free-shipping","expressPay":true,"timeoutMs":2500}
  u-multi      -> {"page":"checkout-v1","banner":"free-shipping","expressPay":true,"timeoutMs":2500}

Why did show-new-checkout evaluate that way?
  u-anonymous  -> value=false reason={"kind":"FALLTHROUGH"}
  u-gold       -> value=true reason={"kind":"RULE_MATCH","ruleId":"gold-plan","ruleIndex":0}

Done. Client closed.
EXIT:0
Thinking…

Both modes work and exit quickly. Multi-kind context: org has betaTester true but rule matches on 'user' kind only — correct behavior (demonstrates kind targeting). Good.

Now test the manage-flag script dry-run and no-token error path.

cd /sandbox/repo/launchdarkly-example && timeout 30 node scripts/manage-flag.js create show-new-checkout --name "Show new checkout" --dry-run; echo "---"; timeout 30 node scripts/manage-flag.js rollout show-new-checkout 25 --dry-run; echo "---"; timeout 30 node scripts/manage-flag.js off show-new-checkout; echo "EXIT:$?"
POST https://app.launchdarkly.com/api/v2/flags/default/test
POST https://app.launchdarkly.com/api/v2/flags/default/test
{
  "name": "Show new checkout",
  "key": "show-new-checkout",
  "variations": [
    {
      "value": true
    },
    {
      "value": false
    }
  ]
}
---
PATCH https://app.launchdarkly.com/api/v2/flags/default/show-new-checkout
[
  {
    "op": "replace",
    "path": "/environments/test/on",
    "value": true
  },
  {
    "op": "replace",
    "path": "/environments/test/fallthrough",
    "value": {
      "rollout": {
        "variations": [
          {
            "variation": 0,
            "weight": 25000
          },
          {
            "variation": 1,
            "weight": 75000
          }
        ]
      }
    }
  }
]
---
LD_ACCESS_TOKEN is not set. Create one in LaunchDarkly (Account settings -> Authorization),
or re-run with --dry-run to see the request that would be sent.
EXIT:1
Thinking…

Works as intended. Now finalize: package.json scripts, README, .env.example, maybe a client-side example (brief, not runnable without env id — I'll include a short snippet in README rather than a file, or a file src/web-client-example.js marked as browser). Keep light: include in README only.

Also add a small test file? Keep light — skip, but the demo acts as verification. Actually adding a tiny test could be useful; but "stay light". I'll skip tests.

Update package.json with scripts, and write README with pricing summary + how to run + manageability assessment.

Let me check package.json content.

cd /sandbox/repo/launchdarkly-example && cat package.json
{
{
  "name": "launchdarkly-example",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "@launchdarkly/node-server-sdk": "^9.14.4"
  }
}
Thinking…

Write final package.json, .env.example, README.

File edits

1 file

Completed
Writtenlaunchdarkly-example/package.jsonView changes

/sandbox/repo/launchdarkly-example/package.json

Written content · previous contents were not recorded

{
"name": "launchdarkly-example",
"version": "1.0.0",
"private": true,
"description": "Minimal LaunchDarkly feature-flag example: Node server SDK + REST API flag management",
"main": "src/index.js",
"scripts": {
"demo": "node src/index.js",
"demo:file": "LD_FLAG_FILE=1 node src/index.js",
"demo:live": "node src/index.js",
"flag": "node scripts/manage-flag.js"
},
"license": "MIT",
"dependencies": {
"@launchdarkly/node-server-sdk": "^9.14.4"
}
}

File edits

1 file

Completed
Writtenlaunchdarkly-example/.env.exampleView changes

/sandbox/repo/launchdarkly-example/.env.example

Written content · previous contents were not recorded

# Copy to .env (or export in your shell). All values are optional: with none of
# them set, the demo runs fully offline using TestData.
# --- SDK (evaluation) ---
# Server-side SDK key for one environment: LaunchDarkly -> Project settings -> Environments
# When set, src/index.js talks to the hosted LaunchDarkly service.
LD_SDK_KEY=
# Set to 1 to read flags from flags/flags.json instead (no network).
LD_FLAG_FILE=
# debug | info | warn | error
LD_LOG_LEVEL=warn
# --- REST API (flag management) ---
# Personal access token: LaunchDarkly -> Account settings -> Authorization
LD_ACCESS_TOKEN=
LD_PROJECT_KEY=default
LD_ENV_KEY=test
Thinking…

Now README with pricing (accurate from page), structure, and manageability assessment.

Note .gitignore in repo root already ignores node_modules — good.

Write README.

File edits

1 file

Completed
Writtenlaunchdarkly-example/README.mdView changes

/sandbox/repo/launchdarkly-example/README.md

Written content · previous contents were not recorded

# LaunchDarkly example (Node server SDK + REST API)
A minimal, runnable feature-flag example against LaunchDarkly's **hosted** service.
No local services, no containers, no long-running processes: the demo evaluates
flags and exits.
## Files
| Path | What it is |
| --- | --- |
| `src/client.js` | Client factory. Picks `live` / `file` / `testdata` mode from env vars. |
| `src/app.js` | Application logic. Asks for flag values, always passes a safe default. |
| `src/index.js` | Runnable demo: evaluates flags for several contexts, prints the reason, reacts to a flag change. |
| `flags/flags.json` | Flag definitions used by the offline `file` mode (CI, local dev). |
| `scripts/manage-flag.js` | Create/inspect/change flags through the LaunchDarkly REST API (`/api/v2`). |
| `.env.example` | Every env var used, with where to get it. |
## Run it
```bash
npm install
npm run demo # offline, deterministic (TestData) - no credentials needed
npm run demo:file # offline, flags come from flags/flags.json
```
Both finish in ~1s and exit with status 0.
Against the real service (only this needs a credential):
```bash
export LD_SDK_KEY=sdk-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Project settings -> Environments
npm run demo:live
```
If LaunchDarkly is unreachable, the SDK logs a warning and serves the defaults you
pass to `variation()` — the app still renders. That is the behaviour you want in
production; a flag outage should not take the app down.
## Managing flags from code
```bash
export LD_ACCESS_TOKEN=api-xxxx # Account settings -> Authorization
export LD_PROJECT_KEY=default LD_ENV_KEY=test
npm run flag list
npm run flag create show-new-checkout -- --name "Show new checkout"
npm run flag rollout show-new-checkout 25 # 25% rollout
npm run flag on show-new-checkout
npm run flag off show-new-checkout # kill switch
```
Append `--dry-run` to any command to print the HTTP request instead of sending it.
Without a token the script fails fast and tells you what to set — it never guesses.
Rollout weights in the API are hundredths of a percent (`25000` = 25%), which the
script converts for you.
## Client-side note
Browser/mobile SDKs use an **environment ID** or mobile key, never the server SDK
key, and they only receive flags marked as available client-side:
```js
import { initialize } from '@launchdarkly/js-client-sdk';
const client = initialize({
clientSideID: '<environment-id>',
context: { kind: 'user', key: 'u-123', country: 'US' },
});
await client.waitForInitialization({ timeout: 5 });
const showBanner = client.variation('promo-banner', false);
```
Server-side evaluation (this repo) keeps targeting rules and secrets off the client;
use it for anything entitlement- or security-related.
## How LaunchDarkly pricing works
Source: launchdarkly.com/pricing (fetched while building this example). No per-seat
charges on any plan — billing is usage-based.
**Developer — $0/mo, forever** (no credit card)
- Unlimited seats; unlimited feature flags; 30 SDKs.
- 1 project and 3 environments.
- 5 service connections/mo (a service connection = one server-side SDK connected to
one environment for a month) and 1,000 client-side MAU/mo.
- 100K experimentation MAU, 10M logs + 10M traces, 5K session replays + 5K errors,
5K AI runs, 14-day data retention.
**Foundation — pay as you go** (14-day trial, then usage)
- $10 per service connection/mo; first 5 included each month. Serverless and
short-lived processes are billed fractionally by connection time.
- $8.33 per 1,000 client-side MAU/mo (a unique client-side user or device).
- 5K AI runs included, then $5 per additional 1,000 runs; observability usage beyond
the included allowances bills as you go; 30-day retention.
- Unlimited projects and environments.
- Special negotiated rates for Kubernetes, serverless, Python and Ruby, because those
architectures create many short-lived server-side connections.
**Enterprise — custom pricing**
- Everything in Foundation plus bulk/large-segment targeting, custom roles and teams,
approval workflows and scheduled flag changes, Release Assistant, SCIM, 100-day
retention, contracted AI-run volumes, tighter config-delivery SLA.
- SSO/SAML is included on every plan.
**Guardian — paid add-on on Enterprise**: guardrail metrics, proactive failure
notifications, automatic pause/rollback, exposure insights.
Cost levers that matter in practice: number of environments you connect server-side
SDKs to (each SDK×environment pair is a service connection), client-side MAU, and
data retention. Note the pricing page's comparison table lists client-side MAU at
"$10/mo per 1K MAU" while the Foundation section lists $8.33 per 1K — confirm the
current number with LaunchDarkly before budgeting.
## Is this easy for an agent to manage?
Yes — this is one of the more agent-friendly products in its category:
- **Runs without infrastructure.** The SDK evaluates locally in-process; there is no
broker, database or sidecar to stand up. The demo above ran, printed results and
exited in about a second.
- **Works credential-free.** `TestData` and the file data source let you write, run
and verify flag logic with no account, so changes are testable in CI.
- **Full REST API.** Flags, rules, rollouts and segments are all manageable with
plain HTTPS + JSON Patch (`scripts/manage-flag.js`), so an agent can create a flag,
ramp a rollout and flip a kill switch with `curl`-grade tooling. A `--dry-run`
style preview makes those mutations safe to review before sending.
- **Small, stable surface.** `init` → `waitForInitialization` → `variation(key,
context, default)`. Defaults mean a misconfiguration degrades instead of throwing.
Friction points worth knowing:
- **Two different credentials** with different scopes (SDK key per environment for
evaluation, access token for the API). Easy to mix up; a leaked SDK key exposes
flag configuration, a leaked token allows writes.
- **API mutations are JSON Patch against nested paths** like
`/environments/test/fallthrough`, and rollout weights are in hundredths of a
percent. Correct but unforgiving of typos; wrap them in helpers as done here.
- **Deprecated package names** still rank highly (`launchdarkly-node-server-sdk` is
superseded by `@launchdarkly/node-server-sdk`), so it's easy to install the legacy
SDK by accident.
- **Some rules can't be expressed in TestData** (operators other than in/not in,
percentage rollouts), so offline tests can't cover every dashboard feature.
- **Live verification needs an account.** I could not exercise the real API or see
live streaming updates here, because no credentials are available in this
environment. Everything above ran offline; the API paths and patch shapes come
from LaunchDarkly's documented v2 API and the `--dry-run` output.
Thinking…

Verify README commands work as documented: npm run flag create show-new-checkout -- --name "..." — with npm run, args after -- are appended. In my script, rest = args after arg. --dry-run check uses process.argv includes. Good. But npm run flag rollout show-new-checkout 25 → rest[0]='25'. Good.

Test npm run demo and npm run flag list --dry-run style: npm run flag list -- --dry-run. Let me verify a couple.

Also verify client-side SDK snippet package name exists (@launchdarkly/js-client-sdk) — yes it does. I won't install it (stay light). Maybe quickly check registry for existence.

cd /sandbox/repo/launchdarkly-example && timeout 60 npm run demo 2>&1 | tail -6 && timeout 30 npm run --silent flag list -- --dry-run && timeout 20 curl -sS -o /dev/null -w "js-client-sdk:%{http_code}\n" https://registry.npmjs.org/@launchdarkly/js-client-sdk && git -C /sandbox/repo status --short
Flag "show-new-checkout" changed in LaunchDarkly (no redeploy needed)
Flag "show-new-checkout" changed in LaunchDarkly (no redeploy needed)
After the change:
  u-anonymous  -> {"page":"checkout-v2","banner":"none","expressPay":false,"timeoutMs":5000}
  u-uk         -> {"page":"checkout-v2","banner":"spring-sale","expressPay":false,"timeoutMs":5000}

Done. Client closed.
GET https://app.launchdarkly.com/api/v2/flags/default?env=test
js-client-sdk:200
?? launchdarkly-example/
Thinking…

All works. Let me do a final read of client.js to ensure clean, and commit? Repo has git; user didn't ask to commit. I'll leave uncommitted but mention. Actually committing might be nice; but guidelines don't require. I'll skip commit (untracked files listed). Hmm, typical harness expects files created; committing optional. I'll leave it.

Final check of client.js content.

read /sandbox/repo/launchdarkly-example/src/client.js{"content":[{"text":"}\n\nfunction modeFromEnv() {\n if (process.env.LD_SDK_KEY) return 'live';\n if (process.env.LD_FLAG_FILE) return 'file';\n return 'test…ctrl+o
{"content":[{"text":"}\n\nfunction modeFromEnv() {\n  if (process.env.LD_SDK_KEY) return 'live';\n  if (process.env.LD_FLAG_FILE) return 'file';\n  return 'testdata';\n}\n\n/**\n * @returns {{ client: import('@launchdarkly/node-server-sdk').LDClient, mode: string, ready: boolean, testData: object|null }}\n */\nasync function createClient({ initTimeoutMs = 5000 } = {}) {\n  const mode = modeFromEnv();\n  const logger = basicLogger({ level: process.env.LD_LOG_LEVEL || 'warn' });\n\n  let options = { logger };\n  let testData = null;\n  if (mode === 'testdata') {\n    // SDK key is not used for evaluation in this mode; a placeholder is required.\n    testData = buildTestData();\n    options = { ...options, updateProcessor: testData.getFactory(), sendEvents: false };\n  } else if (mode === 'file') {\n    const flagsFile = path.join(__dirname, '..', 'flags', 'flags.json');\n    options = {\n      ...options,\n      updateProcessor: new FileDataSourceFactory({ paths: [flagsFile] }).getFactory(),\n      sendEvents: false,\n    };\n  }\n\n  const client = init(process.env.LD_SDK_KEY || 'offline-sdk-key', options);\n\n  let ready = false;\n  try {\n    // timeout is in seconds; if LaunchDarkly is unreachable the SDK keeps serving\n    // the defaults you pass to variation(), so this is never fatal.\n    await client.waitForInitialization({ timeout: Math.ceil(initTimeoutMs / 1000) });\n    ready = true;\n  } catch (err) {\n    logger.warn(`LaunchDarkly did not become ready (${err.message}); falling back to defaults.`);\n  }\n\n  return { client, mode, ready, testData };\n}\n\nmodule.exports = { createClient, FLAGS };\n","type":"text"}]}
Thinking…

Done. Summarize for the user: what I built, how pricing works, manageability verdict, blockers.

LaunchDarkly does have a real developer workflow I could act on (SDK + REST API), so I proceed and report.

Blockers: no credentials → couldn't verify live evaluation or real API mutations; everything else ran offline.

Keep response concise.

LaunchDarkly has a real developer workflow (SDK + REST API), so I built and ran the example. Nothing here needed a local service stack or long-running process — the demo evaluates flags and exits in ~1s.

What's in /sandbox/repo/launchdarkly-example/

PathPurpose
src/client.jsClient factory; picks live (LD_SDK_KEY) / file (flags.json) / testdata (offline, no creds) mode
src/app.jsFlagged app logic — always passes a safe default to variation(), so an outage degrades instead of throwing
src/index.jsRunnable demo: boolean rollout, multivariate string flag, JSON remote config, multi-kind contexts, variationDetail reasons, live flag-change event
flags/flags.jsonOffline flag definitions (targeted contexts + rule) for CI/local dev
scripts/manage-flag.jsFlag management via the v2 REST API: list, create, get, rollout <pct>, on, off, with --dry-run
README.md, .env.exampleRun instructions, client-side snippet, pricing, assessment

Verified: npm run demo and npm run demo:file both exit 0; npm run flag list -- --dry-run prints GET /api/v2/flags/default?env=test; without a token the script fails fast with instructions instead of guessing.

Gotchas I hit and fixed (all documented): the legacy launchdarkly-node-server-sdk package is deprecated in favour of @launchdarkly/node-server-sdk; TestData needs new and getFactory(); TestData rules reference variations by index, not value (passing a string yields MALFORMED_FLAG); update:KEY events emit { key }, not (key, value); waitForInitialization({ timeout }) takes seconds.

Pricing (from launchdarkly.com/pricing, fetched live)

No per-seat charges on any plan — all usage-based.

  • Developer, $0/mo forever: unlimited seats and flags, 1 project / 3 environments, 5 service connections/mo (one server-side SDK × one environment), 1K client-side MAU/mo, 100K experimentation MAU, 10M logs+traces, 5K AI runs, 14-day retention.
  • Foundation, pay-as-you-go: $10 per service connection/mo (first 5 included, serverless billed fractionally by connection time), $8.33 per 1K client-side MAU/mo, 5K AI runs included then $5/1K, unlimited projects/environments, 30-day retention, custom rates for K8s/serverless/Python/Ruby.
  • Enterprise, custom: bulk/large segments, custom roles & teams, approvals/scheduling, Release Assistant, SCIM, 100-day retention, tighter SLA. SSO/SAML on every plan.
  • Guardian: paid Enterprise add-on (guardrail metrics, auto pause/rollback, exposure insights).

Main cost levers: environments you connect server-side SDKs to, client-side MAU, retention. Caveat: their comparison table says "$10/mo per 1K MAU" while the Foundation section says $8.33 — confirm before budgeting.

Easy for me to manage? Yes

In-process evaluation with no broker or sidecar, credential-free testing via TestData/file data sources, and a complete JSON-Patch REST API for flags, rules, rollouts and segments. Small stable surface (init → waitForInitialization → variation). Friction: two differently-scoped credentials (SDK key vs access token), unforgiving nested patch paths with weights in hundredths of a percent, deprecated package names that rank highly, and TestData can't express percentage rollouts or operators beyond in/not-in.

Blocked on one thing only: no LaunchDarkly credentials in this environment, so I could not exercise live streaming evaluation or send real API mutations. Everything else ran and passed offline; the API paths and patch shapes follow the documented v2 API and are visible via --dry-run. Give me LD_SDK_KEY and/or LD_ACCESS_TOKEN and I'll verify the live path.

AX Fundamentals

23 checks. Open an area for details.

This check is a quick preview. Test more tasks and models with Gauge Agents.

Test your product with Gauge Agents

AX Check by Gauge