From 4248319c1ffbb8014eb545e87750a22b7949a150 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Tue, 2 Jun 2026 18:55:32 -0400 Subject: [PATCH 1/5] chore(OF-node-server): adding example application --- .../openfeature-node-server-nightly.yaml | 20 ++++ .../workflows/openfeature-node-server.yaml | 14 +++ package.json | 3 +- .../examples/getting-started/README.md | 36 ++++++++ .../examples/getting-started/e2e.mjs | 27 ++++++ .../examples/getting-started/package.json | 42 +++++++++ .../examples/getting-started/src/index.ts | 92 +++++++++++++++++++ .../examples/getting-started/tsconfig.json | 16 ++++ release-please-config.json | 14 ++- 9 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/openfeature-node-server-nightly.yaml create mode 100644 packages/sdk/openfeature-node-server/examples/getting-started/README.md create mode 100644 packages/sdk/openfeature-node-server/examples/getting-started/e2e.mjs create mode 100644 packages/sdk/openfeature-node-server/examples/getting-started/package.json create mode 100644 packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts create mode 100644 packages/sdk/openfeature-node-server/examples/getting-started/tsconfig.json diff --git a/.github/workflows/openfeature-node-server-nightly.yaml b/.github/workflows/openfeature-node-server-nightly.yaml new file mode 100644 index 0000000000..e10c8e3c50 --- /dev/null +++ b/.github/workflows/openfeature-node-server-nightly.yaml @@ -0,0 +1,20 @@ +name: sdk/openfeature-node-server nightly + +on: + schedule: + - cron: '0 1 * * *' + +jobs: + run-getting-started-example: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./actions/setup-yarn + with: + node-version: 20 + - uses: ./actions/run-example + with: + workspace_name: '@launchdarkly/hello-openfeature-node-server' + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} diff --git a/.github/workflows/openfeature-node-server.yaml b/.github/workflows/openfeature-node-server.yaml index 312cfbf315..43b27bed55 100644 --- a/.github/workflows/openfeature-node-server.yaml +++ b/.github/workflows/openfeature-node-server.yaml @@ -32,3 +32,17 @@ jobs: workspace_name: '@launchdarkly/openfeature-node-server' workspace_path: packages/sdk/openfeature-node-server # TODO: Add contract tests + + run-getting-started-example: + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: ./actions/setup-yarn + with: + node-version: 20 + - uses: ./actions/run-example + with: + workspace_name: '@launchdarkly/hello-openfeature-node-server' + aws_assume_role: ${{ vars.AWS_ROLE_ARN }} diff --git a/package.json b/package.json index 762c06e5c9..8fd0290ed4 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,8 @@ "packages/sdk/shopify-oxygen/example", "packages/sdk/browser/example", "packages/sdk/browser/example-fdv2", - "packages/sdk/openfeature-node-server" + "packages/sdk/openfeature-node-server", + "packages/sdk/openfeature-node-server/examples/getting-started" ], "private": true, "scripts": { diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/README.md b/packages/sdk/openfeature-node-server/examples/getting-started/README.md new file mode 100644 index 0000000000..847754c512 --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/README.md @@ -0,0 +1,36 @@ +# LaunchDarkly sample OpenFeature provider application for Node.js + +We've built a simple console application that demonstrates how to use the +LaunchDarkly OpenFeature provider for Node.js to evaluate flags through the +[OpenFeature](https://openfeature.dev/) interface. + +Below, you'll find the build procedure. For more comprehensive instructions, you +can visit your [Quickstart page](https://app.launchdarkly.com/quickstart#/) or +the [Node.js (server-side) reference guide](https://docs.launchdarkly.com/sdk/server-side/node-js). + +This demo requires Node.js 20 or higher. + +## Build instructions + +1. Set the environment variable `LAUNCHDARKLY_SDK_KEY` to your LaunchDarkly SDK + key. If there is an existing boolean feature flag in your LaunchDarkly + project that you want to evaluate, set `LAUNCHDARKLY_FLAG_KEY` to the flag + key; otherwise, a boolean flag of `sample-feature` will be assumed. + + ```bash + export LAUNCHDARKLY_SDK_KEY="my-sdk-key" + export LAUNCHDARKLY_FLAG_KEY="my-boolean-flag" + ``` + +2. From the example directory, install dependencies and run the example: + + ```bash + yarn start + ``` + + You should receive the message: + + > The {flagKey} feature flag evaluates to {flagValue}. + +The application will run continuously and react to flag changes in LaunchDarkly. +Toggle the flag in the LaunchDarkly dashboard to watch the demo re-evaluate live. diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/e2e.mjs b/packages/sdk/openfeature-node-server/examples/getting-started/e2e.mjs new file mode 100644 index 0000000000..71174b108b --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/e2e.mjs @@ -0,0 +1,27 @@ +import { spawnSync } from 'node:child_process'; + +// The Hello app prints "The feature flag evaluates to ." on startup. +// The hello-world-demo boolean flag always returns true, so a successful run must contain this. +const EXPECTED = 'feature flag evaluates to true'; + +// Force one-shot mode so the continuously-running app exits after the initial evaluation. +const result = spawnSync('node', ['./dist/index.js'], { + encoding: 'utf8', + timeout: 60_000, + env: { ...process.env, CI: '1' }, +}); + +process.stdout.write(result.stdout ?? ''); +process.stderr.write(result.stderr ?? ''); + +if (result.status !== 0) { + console.error(`\n*** e2e failed: app exited with status ${result.status}.`); + process.exit(1); +} + +if (!(result.stdout ?? '').includes(EXPECTED)) { + console.error(`\n*** e2e failed: expected output to contain "${EXPECTED}".`); + process.exit(1); +} + +console.log(`\n*** e2e passed: output contained "${EXPECTED}".`); diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/package.json b/packages/sdk/openfeature-node-server/examples/getting-started/package.json new file mode 100644 index 0000000000..d3e9fb99ae --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/package.json @@ -0,0 +1,42 @@ +{ + "name": "@launchdarkly/hello-openfeature-node-server", + "version": "0.1.0", + "description": "Hello LaunchDarkly OpenFeature Provider for the Node.js Server-Side SDK", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "start": "yarn build && node ./dist/index.js", + "test": "node ./e2e.mjs", + "lint": "npx eslint . --ext .ts", + "lint:fix": "yarn run lint --fix", + "check": "yarn lint && yarn build" + }, + "keywords": [ + "launchdarkly", + "openfeature", + "feature-flags" + ], + "author": "LaunchDarkly", + "license": "Apache-2.0", + "dependencies": { + "@launchdarkly/node-server-sdk": "9.10.14", + "@launchdarkly/openfeature-node-server": "1.2.0", + "@openfeature/core": "^1.10.0", + "@openfeature/server-sdk": "^1.16.0" + }, + "devDependencies": { + "@tsconfig/node20": "20.1.4", + "@types/node": "^25.9.1", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "eslint": "^8.45.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-prettier": "^5.0.0", + "prettier": "^3.0.0", + "typescript": "5.1.6" + } +} diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts b/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts new file mode 100644 index 0000000000..23ca3cc6e3 --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts @@ -0,0 +1,92 @@ +/* eslint-disable no-console */ +import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk'; + +import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server'; + +// The server-side SDK key is read from the LAUNCHDARKLY_SDK_KEY environment variable. +const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY; + +// Set flagKey to the feature flag key you want to evaluate. +const flagKey = process.env.LAUNCHDARKLY_FLAG_KEY || 'sample-feature'; + +if (!sdkKey) { + console.error( + '*** LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.', + ); + process.exit(1); +} + +if (!flagKey) { + console.error( + "*** LaunchDarkly flag key is required: set the 'flagKey' variable in src/index.ts, or the LAUNCHDARKLY_FLAG_KEY environment variable and try again.", + ); + process.exit(1); +} + +const BANNER = ` ██ + ██ + ████████ + ███████ +██ LAUNCHDARKLY █ + ███████ + ████████ + ██ + ██ +`; + +// Set up the evaluation context. This context should appear on your LaunchDarkly contexts dashboard +// soon after you run the demo. +const context = { + kind: 'user', + targetingKey: 'example-user-key', + name: 'Sandy', +}; + +let lastFlagValue: boolean | null = null; + +function printFlagState(flagValue: boolean): void { + if (lastFlagValue === flagValue) { + return; + } + console.log(`*** The '${flagKey}' feature flag evaluates to ${flagValue}.\n`); + if (flagValue) { + console.log(BANNER); + } + lastFlagValue = flagValue; +} + +async function main(): Promise { + const provider = new LaunchDarklyProvider(sdkKey!); + + try { + await OpenFeature.setProviderAndWait(provider); + console.log('*** SDK successfully initialized!\n'); + } catch (error) { + console.error( + `*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.\n${error}`, + ); + process.exit(1); + } + + const ofClient = OpenFeature.getClient(); + const initialValue = await ofClient.getBooleanValue(flagKey, false, context); + printFlagState(initialValue); + + // Subscribe to provider configuration changes so the demo reacts to LaunchDarkly flag updates + // without polling. + ofClient.addHandler(ProviderEvents.ConfigurationChanged, async (eventDetails) => { + if (eventDetails?.flagsChanged?.includes(flagKey)) { + const updatedValue = await ofClient.getBooleanValue(flagKey, false, context); + printFlagState(updatedValue); + } + }); + + if (process.env.CI !== undefined) { + process.exit(0); + } +} + +main().catch((error) => { + console.error(`*** Unhandled error: ${error}`); + process.exit(1); +}); diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/tsconfig.json b/packages/sdk/openfeature-node-server/examples/getting-started/tsconfig.json new file mode 100644 index 0000000000..737b952241 --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "@tsconfig/node20/tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "sourceMap": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false + }, + "include": ["src"], + "exclude": ["dist", "node_modules"] +} diff --git a/release-please-config.json b/release-please-config.json index 1aa5e95106..f4702f5229 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -209,6 +209,11 @@ "type": "json", "path": "/packages/sdk/server-ai/examples/agent-graph-traversal/package.json", "jsonpath": "$.dependencies['@launchdarkly/node-server-sdk']" + }, + { + "type": "json", + "path": "/packages/sdk/openfeature-node-server/examples/getting-started/package.json", + "jsonpath": "$.dependencies['@launchdarkly/node-server-sdk']" } ] }, @@ -347,7 +352,14 @@ }, "packages/sdk/openfeature-node-server": { "bump-minor-pre-major": true, - "extra-files": ["src/LaunchDarklyProvider.ts"] + "extra-files": [ + "src/LaunchDarklyProvider.ts", + { + "type": "json", + "path": "/packages/sdk/openfeature-node-server/examples/getting-started/package.json", + "jsonpath": "$.dependencies['@launchdarkly/openfeature-node-server']" + } + ] } }, "plugins": [ From 01546e585d1a7a54674c7396e5c32467a7f27331 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Wed, 3 Jun 2026 15:32:53 -0400 Subject: [PATCH 2/5] refactor: update eslint --- .../{e2e.mjs => e2e/verify.mjs} | 0 .../examples/getting-started/package.json | 18 +++++++++--------- .../examples/getting-started/src/index.ts | 1 - 3 files changed, 9 insertions(+), 10 deletions(-) rename packages/sdk/openfeature-node-server/examples/getting-started/{e2e.mjs => e2e/verify.mjs} (100%) diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/e2e.mjs b/packages/sdk/openfeature-node-server/examples/getting-started/e2e/verify.mjs similarity index 100% rename from packages/sdk/openfeature-node-server/examples/getting-started/e2e.mjs rename to packages/sdk/openfeature-node-server/examples/getting-started/e2e/verify.mjs diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/package.json b/packages/sdk/openfeature-node-server/examples/getting-started/package.json index d3e9fb99ae..401840ce3c 100644 --- a/packages/sdk/openfeature-node-server/examples/getting-started/package.json +++ b/packages/sdk/openfeature-node-server/examples/getting-started/package.json @@ -9,8 +9,8 @@ "scripts": { "build": "tsc", "start": "yarn build && node ./dist/index.js", - "test": "node ./e2e.mjs", - "lint": "npx eslint . --ext .ts", + "test": "node ./e2e/verify.mjs", + "lint": "npx eslint .", "lint:fix": "yarn run lint --fix", "check": "yarn lint && yarn build" }, @@ -28,15 +28,15 @@ "@openfeature/server-sdk": "^1.16.0" }, "devDependencies": { + "@eslint/js": "^9.0.0", "@tsconfig/node20": "20.1.4", "@types/node": "^25.9.1", - "@typescript-eslint/eslint-plugin": "^6.20.0", - "@typescript-eslint/parser": "^6.20.0", - "eslint": "^8.45.0", - "eslint-config-prettier": "^8.8.0", - "eslint-plugin-import": "^2.27.5", - "eslint-plugin-prettier": "^5.0.0", + "eslint": "^9.0.0", + "eslint-import-resolver-typescript": "^4.0.0", + "eslint-plugin-import-x": "^4.0.0", + "globals": "^16.0.0", "prettier": "^3.0.0", - "typescript": "5.1.6" + "typescript": "5.1.6", + "typescript-eslint": "^8.0.0" } } diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts b/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts index 23ca3cc6e3..fc8ac99fb4 100644 --- a/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts +++ b/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts @@ -1,4 +1,3 @@ -/* eslint-disable no-console */ import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk'; import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server'; From ca3eecf6c60a6c0dc38bcc7a95169bcd7b43f234 Mon Sep 17 00:00:00 2001 From: joker23 <2494686+joker23@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:30:47 -0400 Subject: [PATCH 3/5] Update .github/workflows/openfeature-node-server.yaml Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/openfeature-node-server.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openfeature-node-server.yaml b/.github/workflows/openfeature-node-server.yaml index 43b27bed55..bce669ec2a 100644 --- a/.github/workflows/openfeature-node-server.yaml +++ b/.github/workflows/openfeature-node-server.yaml @@ -38,7 +38,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./actions/setup-yarn with: node-version: 20 From 5b8155061ad5df78f9ed73dff714256c4c57f533 Mon Sep 17 00:00:00 2001 From: joker23 <2494686+joker23@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:30:54 -0400 Subject: [PATCH 4/5] Update .github/workflows/openfeature-node-server-nightly.yaml Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .github/workflows/openfeature-node-server-nightly.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openfeature-node-server-nightly.yaml b/.github/workflows/openfeature-node-server-nightly.yaml index e10c8e3c50..0648f3a2a5 100644 --- a/.github/workflows/openfeature-node-server-nightly.yaml +++ b/.github/workflows/openfeature-node-server-nightly.yaml @@ -10,7 +10,7 @@ jobs: permissions: id-token: write steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - uses: ./actions/setup-yarn with: node-version: 20 From 4ba1226640fa4da9333fece6507956a1f73922f1 Mon Sep 17 00:00:00 2001 From: Steven Zhang Date: Thu, 4 Jun 2026 13:49:07 -0400 Subject: [PATCH 5/5] chore: bot comments --- .../examples/getting-started/package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sdk/openfeature-node-server/examples/getting-started/package.json b/packages/sdk/openfeature-node-server/examples/getting-started/package.json index 401840ce3c..11e1335f06 100644 --- a/packages/sdk/openfeature-node-server/examples/getting-started/package.json +++ b/packages/sdk/openfeature-node-server/examples/getting-started/package.json @@ -22,8 +22,8 @@ "author": "LaunchDarkly", "license": "Apache-2.0", "dependencies": { - "@launchdarkly/node-server-sdk": "9.10.14", - "@launchdarkly/openfeature-node-server": "1.2.0", + "@launchdarkly/node-server-sdk": "9.11.1", + "@launchdarkly/openfeature-node-server": "1.2.2", "@openfeature/core": "^1.10.0", "@openfeature/server-sdk": "^1.16.0" },