-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Update LaunchDarkly Flags SDK template for Marketplace integration #1525
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,31 @@ | ||
| # For @flags-sdk/launchdarkly | ||
| # The Flags Explorer uses this to securely read and override flags. | ||
| FLAGS_SECRET="" | ||
|
|
||
| # Server-side flags are read in one of two modes. Configure ONE of them. | ||
|
|
||
| # Mode 1 (recommended): Edge Config | ||
| # Flags are read from a Vercel Edge Config store that the LaunchDarkly | ||
| # Marketplace integration keeps in sync. Lowest latency. | ||
| # Set EITHER EDGE_CONFIG or EXPERIMENTATION_CONFIG (both are Edge Config | ||
| # connection strings). The Marketplace integration injects EXPERIMENTATION_CONFIG | ||
| # when you enable Edge Config syncing; EDGE_CONFIG takes precedence if both are set. | ||
| EDGE_CONFIG="" | ||
| LAUNCHDARKLY_PROJECT_SLUG="" | ||
| EXPERIMENTATION_CONFIG="" | ||
| LAUNCHDARKLY_CLIENT_SIDE_ID="" | ||
|
|
||
| # For launchdarkly-react-client-sdk | ||
| # Mode 2: LaunchDarkly server SDK | ||
| # Flags are read directly from LaunchDarkly using a server SDK key. | ||
| # No Edge Config required. Used automatically when neither EDGE_CONFIG nor | ||
| # EXPERIMENTATION_CONFIG is set. | ||
| LAUNCHDARKLY_SDK_KEY="" | ||
|
|
||
| # Required by the client-side React SDK (auto-injected by the integration). | ||
| NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_SIDE_ID="" | ||
|
|
||
| # For Flags Explorer | ||
| # Optional: links each flag to its details page in the LaunchDarkly console. | ||
| LAUNCHDARKLY_PROJECT_SLUG="" | ||
|
|
||
| # Optional: lets the Flags Explorer fetch extra flag metadata from the LD API. | ||
| LAUNCHDARKLY_API_KEY="" | ||
| LAUNCHDARKLY_PROJECT_KEY="" | ||
| LAUNCHDARKLY_ENVIRONMENT="" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,8 @@ yarn-debug.log* | |
| yarn-error.log* | ||
|
|
||
| # local env files | ||
| .env*.local | ||
| .env* | ||
| !.env.example | ||
|
|
||
| # vercel | ||
| .vercel | ||
|
|
||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is just the linter |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import type { Adapter } from 'flags' | ||
| import { | ||
| createLaunchDarklyAdapter, | ||
| type LDContext, | ||
| } from '@flags-sdk/launchdarkly' | ||
|
|
||
| export type { LDContext } | ||
|
|
||
| /** | ||
| * This adapter supports two ways of reading LaunchDarkly flags, and picks one | ||
| * automatically based on which environment variables are present: | ||
| * | ||
| * 1. Edge Config mode (`EDGE_CONFIG` or `EXPERIMENTATION_CONFIG`, plus | ||
|
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This template originally had |
||
| * `LAUNCHDARKLY_CLIENT_SIDE_ID`): reads flags from a Vercel Edge Config | ||
| * store using `@flags-sdk/launchdarkly`. This is edge-native and the lowest | ||
| * latency, and is what the LaunchDarkly Vercel Marketplace integration is | ||
| * designed for. The Marketplace integration injects the connection string | ||
| * as `EXPERIMENTATION_CONFIG` when Edge Config syncing is enabled; a | ||
| * manually-provisioned store is typically `EDGE_CONFIG`. Either works. | ||
| * | ||
| * 2. Server SDK mode (`LAUNCHDARKLY_SDK_KEY`): talks to LaunchDarkly directly | ||
| * using `@launchdarkly/node-server-sdk`. This requires no Edge Config, so | ||
| * the template works with just the SDK key. | ||
| * | ||
| * Edge Config mode is preferred when both are configured. | ||
| */ | ||
|
|
||
| // The Marketplace integration provides the Edge Config connection string as | ||
| // `EXPERIMENTATION_CONFIG`; a manually-created store is usually `EDGE_CONFIG`. | ||
| // Both are valid Edge Config URLs, so accept either (EDGE_CONFIG wins if set). | ||
| const edgeConfigConnectionString = | ||
| process.env.EDGE_CONFIG ?? process.env.EXPERIMENTATION_CONFIG | ||
| const clientSideId = process.env.LAUNCHDARKLY_CLIENT_SIDE_ID | ||
| const projectSlug = process.env.LAUNCHDARKLY_PROJECT_SLUG | ||
| const sdkKey = process.env.LAUNCHDARKLY_SDK_KEY | ||
|
|
||
| /** Links a flag to its details page in the LaunchDarkly console. */ | ||
| function flagOrigin(key: string): string | undefined { | ||
| return projectSlug | ||
| ? `https://app.launchdarkly.com/projects/${projectSlug}/flags/${key}/` | ||
| : undefined | ||
| } | ||
|
|
||
| /** The subset of the LaunchDarkly client this app relies on. */ | ||
| interface LDClientLike { | ||
| waitForInitialization(): Promise<unknown> | ||
| allFlagsState(context: LDContext): Promise<{ toJSON(): unknown } | undefined> | ||
| } | ||
|
|
||
| interface LDAdapterLike { | ||
| variation<ValueType = unknown>(): Adapter<ValueType, LDContext> | ||
| ldClient: LDClientLike | ||
| } | ||
|
|
||
| function createEdgeConfigAdapter(): LDAdapterLike { | ||
| return createLaunchDarklyAdapter({ | ||
| projectSlug: projectSlug ?? '', | ||
| clientSideId: clientSideId!, | ||
| edgeConfigConnectionString: edgeConfigConnectionString!, | ||
| }) as unknown as LDAdapterLike | ||
| } | ||
|
|
||
| function createServerSdkAdapter(): LDAdapterLike { | ||
| // Lazily initialize a single client so we only connect when this mode runs. | ||
| let clientPromise: | ||
| | Promise<import('@launchdarkly/node-server-sdk').LDClient> | ||
| | undefined | ||
|
|
||
| async function getClient() { | ||
| if (!clientPromise) { | ||
| clientPromise = import('@launchdarkly/node-server-sdk').then( | ||
| async ({ init }) => { | ||
| const client = init(sdkKey!) | ||
| await client.waitForInitialization({ timeout: 10 }) | ||
| return client | ||
| } | ||
| ) | ||
| } | ||
| return clientPromise | ||
| } | ||
|
|
||
| return { | ||
| variation<ValueType = unknown>(): Adapter<ValueType, LDContext> { | ||
| return { | ||
| origin: flagOrigin, | ||
| async decide({ key, entities, defaultValue }) { | ||
| const client = await getClient() | ||
| const context: LDContext = entities ?? { | ||
| kind: 'user', | ||
| key: 'anonymous', | ||
| anonymous: true, | ||
| } | ||
| return client.variation( | ||
| key, | ||
| context, | ||
| defaultValue | ||
| ) as Promise<ValueType> | ||
| }, | ||
| } | ||
| }, | ||
| ldClient: { | ||
| waitForInitialization: () => getClient(), | ||
| async allFlagsState(context: LDContext) { | ||
| const client = await getClient() | ||
| return (await client.allFlagsState(context)) as unknown as { | ||
| toJSON(): unknown | ||
| } | ||
| }, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| let resolved: LDAdapterLike | undefined | ||
|
|
||
| // Resolve the mode lazily (on first use) so importing this module never throws | ||
| // at build time when env vars are not yet available. | ||
| function adapter(): LDAdapterLike { | ||
| if (resolved) return resolved | ||
| if (edgeConfigConnectionString && clientSideId) { | ||
| resolved = createEdgeConfigAdapter() | ||
| } else if (sdkKey) { | ||
| resolved = createServerSdkAdapter() | ||
| } else { | ||
| throw new Error( | ||
| 'LaunchDarkly is not configured. Set EDGE_CONFIG (or EXPERIMENTATION_CONFIG) ' + | ||
| 'and LAUNCHDARKLY_CLIENT_SIDE_ID for Edge Config mode, or LAUNCHDARKLY_SDK_KEY ' + | ||
| 'for server SDK mode.' | ||
| ) | ||
| } | ||
| return resolved | ||
| } | ||
|
|
||
| export const ldAdapter: LDAdapterLike = { | ||
| variation<ValueType = unknown>(): Adapter<ValueType, LDContext> { | ||
| let inner: Adapter<ValueType, LDContext> | undefined | ||
| return { | ||
| origin: flagOrigin, | ||
| decide(options) { | ||
| if (!inner) inner = adapter().variation<ValueType>() | ||
| return inner.decide(options) | ||
| }, | ||
| } | ||
| }, | ||
| ldClient: { | ||
| waitForInitialization: () => adapter().ldClient.waitForInitialization(), | ||
| allFlagsState: (context) => adapter().ldClient.allFlagsState(context), | ||
| }, | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not too familiar, can you just do this?