Skip to content

Commit aefee1b

Browse files
chore(OF-node-server): adding example application (#1411)
This PR will add an example application for openfeature node server provider <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Changes are limited to a new example app, docs, and CI/release-please wiring; no production SDK behavior is modified. > > **Overview** > Adds a **getting-started console example** (`@launchdarkly/hello-openfeature-node-server`) that wires **LaunchDarklyProvider** through OpenFeature, evaluates a boolean flag, and listens for **ConfigurationChanged** updates; it exits after the first evaluation when **`CI`** is set for automated runs. > > **CI and release tooling:** the example workspace is registered in the root **`package.json`**, **`run-getting-started-example`** jobs run it via **`./actions/run-example`** (with AWS role) on the main OpenFeature workflow and on a new **nightly** schedule, and **`release-please-config.json`** is updated so releases bump SDK dependency pins in the example. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4ba1226. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: devin-ai-integration[bot] <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 176a6a1 commit aefee1b

9 files changed

Lines changed: 261 additions & 2 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: sdk/openfeature-node-server nightly
2+
3+
on:
4+
schedule:
5+
- cron: '0 1 * * *'
6+
7+
jobs:
8+
run-getting-started-example:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
id-token: write
12+
steps:
13+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
14+
- uses: ./actions/setup-yarn
15+
with:
16+
node-version: 20
17+
- uses: ./actions/run-example
18+
with:
19+
workspace_name: '@launchdarkly/hello-openfeature-node-server'
20+
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}

.github/workflows/openfeature-node-server.yaml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,17 @@ jobs:
3232
workspace_name: '@launchdarkly/openfeature-node-server'
3333
workspace_path: packages/sdk/openfeature-node-server
3434
# TODO: Add contract tests
35+
36+
run-getting-started-example:
37+
runs-on: ubuntu-latest
38+
permissions:
39+
id-token: write
40+
steps:
41+
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
42+
- uses: ./actions/setup-yarn
43+
with:
44+
node-version: 20
45+
- uses: ./actions/run-example
46+
with:
47+
workspace_name: '@launchdarkly/hello-openfeature-node-server'
48+
aws_assume_role: ${{ vars.AWS_ROLE_ARN }}

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@
6161
"packages/sdk/shopify-oxygen/example",
6262
"packages/sdk/browser/example",
6363
"packages/sdk/browser/example-fdv2",
64-
"packages/sdk/openfeature-node-server"
64+
"packages/sdk/openfeature-node-server",
65+
"packages/sdk/openfeature-node-server/examples/getting-started"
6566
],
6667
"private": true,
6768
"scripts": {
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# LaunchDarkly sample OpenFeature provider application for Node.js
2+
3+
We've built a simple console application that demonstrates how to use the
4+
LaunchDarkly OpenFeature provider for Node.js to evaluate flags through the
5+
[OpenFeature](https://openfeature.dev/) interface.
6+
7+
Below, you'll find the build procedure. For more comprehensive instructions, you
8+
can visit your [Quickstart page](https://app.launchdarkly.com/quickstart#/) or
9+
the [Node.js (server-side) reference guide](https://docs.launchdarkly.com/sdk/server-side/node-js).
10+
11+
This demo requires Node.js 20 or higher.
12+
13+
## Build instructions
14+
15+
1. Set the environment variable `LAUNCHDARKLY_SDK_KEY` to your LaunchDarkly SDK
16+
key. If there is an existing boolean feature flag in your LaunchDarkly
17+
project that you want to evaluate, set `LAUNCHDARKLY_FLAG_KEY` to the flag
18+
key; otherwise, a boolean flag of `sample-feature` will be assumed.
19+
20+
```bash
21+
export LAUNCHDARKLY_SDK_KEY="my-sdk-key"
22+
export LAUNCHDARKLY_FLAG_KEY="my-boolean-flag"
23+
```
24+
25+
2. From the example directory, install dependencies and run the example:
26+
27+
```bash
28+
yarn start
29+
```
30+
31+
You should receive the message:
32+
33+
> The {flagKey} feature flag evaluates to {flagValue}.
34+
35+
The application will run continuously and react to flag changes in LaunchDarkly.
36+
Toggle the flag in the LaunchDarkly dashboard to watch the demo re-evaluate live.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { spawnSync } from 'node:child_process';
2+
3+
// The Hello app prints "The <flagKey> feature flag evaluates to <flagValue>." on startup.
4+
// The hello-world-demo boolean flag always returns true, so a successful run must contain this.
5+
const EXPECTED = 'feature flag evaluates to true';
6+
7+
// Force one-shot mode so the continuously-running app exits after the initial evaluation.
8+
const result = spawnSync('node', ['./dist/index.js'], {
9+
encoding: 'utf8',
10+
timeout: 60_000,
11+
env: { ...process.env, CI: '1' },
12+
});
13+
14+
process.stdout.write(result.stdout ?? '');
15+
process.stderr.write(result.stderr ?? '');
16+
17+
if (result.status !== 0) {
18+
console.error(`\n*** e2e failed: app exited with status ${result.status}.`);
19+
process.exit(1);
20+
}
21+
22+
if (!(result.stdout ?? '').includes(EXPECTED)) {
23+
console.error(`\n*** e2e failed: expected output to contain "${EXPECTED}".`);
24+
process.exit(1);
25+
}
26+
27+
console.log(`\n*** e2e passed: output contained "${EXPECTED}".`);
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "@launchdarkly/hello-openfeature-node-server",
3+
"version": "0.1.0",
4+
"description": "Hello LaunchDarkly OpenFeature Provider for the Node.js Server-Side SDK",
5+
"private": true,
6+
"type": "module",
7+
"main": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"scripts": {
10+
"build": "tsc",
11+
"start": "yarn build && node ./dist/index.js",
12+
"test": "node ./e2e/verify.mjs",
13+
"lint": "npx eslint .",
14+
"lint:fix": "yarn run lint --fix",
15+
"check": "yarn lint && yarn build"
16+
},
17+
"keywords": [
18+
"launchdarkly",
19+
"openfeature",
20+
"feature-flags"
21+
],
22+
"author": "LaunchDarkly",
23+
"license": "Apache-2.0",
24+
"dependencies": {
25+
"@launchdarkly/node-server-sdk": "9.11.1",
26+
"@launchdarkly/openfeature-node-server": "1.2.2",
27+
"@openfeature/core": "^1.10.0",
28+
"@openfeature/server-sdk": "^1.16.0"
29+
},
30+
"devDependencies": {
31+
"@eslint/js": "^9.0.0",
32+
"@tsconfig/node20": "20.1.4",
33+
"@types/node": "^25.9.1",
34+
"eslint": "^9.0.0",
35+
"eslint-import-resolver-typescript": "^4.0.0",
36+
"eslint-plugin-import-x": "^4.0.0",
37+
"globals": "^16.0.0",
38+
"prettier": "^3.0.0",
39+
"typescript": "5.1.6",
40+
"typescript-eslint": "^8.0.0"
41+
}
42+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { OpenFeature, ProviderEvents } from '@openfeature/server-sdk';
2+
3+
import { LaunchDarklyProvider } from '@launchdarkly/openfeature-node-server';
4+
5+
// The server-side SDK key is read from the LAUNCHDARKLY_SDK_KEY environment variable.
6+
const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY;
7+
8+
// Set flagKey to the feature flag key you want to evaluate.
9+
const flagKey = process.env.LAUNCHDARKLY_FLAG_KEY || 'sample-feature';
10+
11+
if (!sdkKey) {
12+
console.error(
13+
'*** LaunchDarkly SDK key is required: set the LAUNCHDARKLY_SDK_KEY environment variable and try again.',
14+
);
15+
process.exit(1);
16+
}
17+
18+
if (!flagKey) {
19+
console.error(
20+
"*** LaunchDarkly flag key is required: set the 'flagKey' variable in src/index.ts, or the LAUNCHDARKLY_FLAG_KEY environment variable and try again.",
21+
);
22+
process.exit(1);
23+
}
24+
25+
const BANNER = ` ██
26+
██
27+
████████
28+
███████
29+
██ LAUNCHDARKLY █
30+
███████
31+
████████
32+
██
33+
██
34+
`;
35+
36+
// Set up the evaluation context. This context should appear on your LaunchDarkly contexts dashboard
37+
// soon after you run the demo.
38+
const context = {
39+
kind: 'user',
40+
targetingKey: 'example-user-key',
41+
name: 'Sandy',
42+
};
43+
44+
let lastFlagValue: boolean | null = null;
45+
46+
function printFlagState(flagValue: boolean): void {
47+
if (lastFlagValue === flagValue) {
48+
return;
49+
}
50+
console.log(`*** The '${flagKey}' feature flag evaluates to ${flagValue}.\n`);
51+
if (flagValue) {
52+
console.log(BANNER);
53+
}
54+
lastFlagValue = flagValue;
55+
}
56+
57+
async function main(): Promise<void> {
58+
const provider = new LaunchDarklyProvider(sdkKey!);
59+
60+
try {
61+
await OpenFeature.setProviderAndWait(provider);
62+
console.log('*** SDK successfully initialized!\n');
63+
} catch (error) {
64+
console.error(
65+
`*** SDK failed to initialize. Please check your internet connection and SDK credential for any typo.\n${error}`,
66+
);
67+
process.exit(1);
68+
}
69+
70+
const ofClient = OpenFeature.getClient();
71+
const initialValue = await ofClient.getBooleanValue(flagKey, false, context);
72+
printFlagState(initialValue);
73+
74+
// Subscribe to provider configuration changes so the demo reacts to LaunchDarkly flag updates
75+
// without polling.
76+
ofClient.addHandler(ProviderEvents.ConfigurationChanged, async (eventDetails) => {
77+
if (eventDetails?.flagsChanged?.includes(flagKey)) {
78+
const updatedValue = await ofClient.getBooleanValue(flagKey, false, context);
79+
printFlagState(updatedValue);
80+
}
81+
});
82+
83+
if (process.env.CI !== undefined) {
84+
process.exit(0);
85+
}
86+
}
87+
88+
main().catch((error) => {
89+
console.error(`*** Unhandled error: ${error}`);
90+
process.exit(1);
91+
});
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"extends": "@tsconfig/node20/tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "dist",
5+
"declaration": true,
6+
"sourceMap": true,
7+
"noUncheckedIndexedAccess": true,
8+
"noUnusedLocals": true,
9+
"noUnusedParameters": true,
10+
"noFallthroughCasesInSwitch": true,
11+
"allowUnreachableCode": false,
12+
"allowUnusedLabels": false
13+
},
14+
"include": ["src"],
15+
"exclude": ["dist", "node_modules"]
16+
}

release-please-config.json

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,11 @@
217217
"type": "json",
218218
"path": "/packages/sdk/server-ai/examples/agent-graph-traversal/package.json",
219219
"jsonpath": "$.dependencies['@launchdarkly/node-server-sdk']"
220+
},
221+
{
222+
"type": "json",
223+
"path": "/packages/sdk/openfeature-node-server/examples/getting-started/package.json",
224+
"jsonpath": "$.dependencies['@launchdarkly/node-server-sdk']"
220225
}
221226
]
222227
},
@@ -374,7 +379,14 @@
374379
},
375380
"packages/sdk/openfeature-node-server": {
376381
"bump-minor-pre-major": true,
377-
"extra-files": ["src/LaunchDarklyProvider.ts"]
382+
"extra-files": [
383+
"src/LaunchDarklyProvider.ts",
384+
{
385+
"type": "json",
386+
"path": "/packages/sdk/openfeature-node-server/examples/getting-started/package.json",
387+
"jsonpath": "$.dependencies['@launchdarkly/openfeature-node-server']"
388+
}
389+
]
378390
}
379391
},
380392
"plugins": [

0 commit comments

Comments
 (0)