diff --git a/.github/workflows/openfeature-node-server-nightly.yaml b/.github/workflows/openfeature-node-server-nightly.yaml new file mode 100644 index 0000000000..0648f3a2a5 --- /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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - 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..bce669ec2a 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@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - 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/verify.mjs b/packages/sdk/openfeature-node-server/examples/getting-started/e2e/verify.mjs new file mode 100644 index 0000000000..71174b108b --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/e2e/verify.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..11e1335f06 --- /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/verify.mjs", + "lint": "npx eslint .", + "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.11.1", + "@launchdarkly/openfeature-node-server": "1.2.2", + "@openfeature/core": "^1.10.0", + "@openfeature/server-sdk": "^1.16.0" + }, + "devDependencies": { + "@eslint/js": "^9.0.0", + "@tsconfig/node20": "20.1.4", + "@types/node": "^25.9.1", + "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-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 new file mode 100644 index 0000000000..fc8ac99fb4 --- /dev/null +++ b/packages/sdk/openfeature-node-server/examples/getting-started/src/index.ts @@ -0,0 +1,91 @@ +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": [