diff --git a/package.json b/package.json index 03acc80b90..25a23cba93 100644 --- a/package.json +++ b/package.json @@ -28,6 +28,7 @@ "packages/sdk/react-native/example-fdv2", "packages/sdk/react-native/contract-tests/entity", "packages/sdk/vercel", + "packages/sdk/vercel/examples/complete", "packages/sdk/svelte", "packages/sdk/svelte/example", "packages/sdk/akamai-base", diff --git a/packages/sdk/react/examples/vercel-edge/package.json b/packages/sdk/react/examples/vercel-edge/package.json index d3f3332de3..30fac9f596 100644 --- a/packages/sdk/react/examples/vercel-edge/package.json +++ b/packages/sdk/react/examples/vercel-edge/package.json @@ -1,5 +1,5 @@ { - "name": "@internal/react-sdk-example-vercel-edge", + "name": "@launchdarkly/react-sdk-example-vercel-edge", "private": true, "scripts": { "dev": "next dev", diff --git a/packages/sdk/vercel/examples/complete/.env.example b/packages/sdk/vercel/examples/complete/.env.example new file mode 100644 index 0000000000..979894296a --- /dev/null +++ b/packages/sdk/vercel/examples/complete/.env.example @@ -0,0 +1,3 @@ +LD_CLIENT_SIDE_ID= +EDGE_CONFIG= +LAUNCHDARKLY_FLAG_KEY=sample-feature diff --git a/packages/sdk/vercel/examples/complete/.eslintrc.json b/packages/sdk/vercel/examples/complete/.eslintrc.json deleted file mode 100644 index 6828b19079..0000000000 --- a/packages/sdk/vercel/examples/complete/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["plugin:@next/next/recommended"] -} diff --git a/packages/sdk/vercel/examples/complete/.gitignore b/packages/sdk/vercel/examples/complete/.gitignore index 34d63f0733..f32bc09598 100644 --- a/packages/sdk/vercel/examples/complete/.gitignore +++ b/packages/sdk/vercel/examples/complete/.gitignore @@ -1,43 +1,20 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# Dependencies +# dependencies /node_modules -/.pnp -.pnp.js - -# Testing -/coverage -# Next.js +# next.js /.next/ /out/ -# VS code -/.vscode - -# Production -/build - -# Misc -.DS_Store -*.pem - -# Debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# Local ENV files -.env.local -.env.development.local -.env.test.local -.env.production.local +# env files +.env* +!.env.example -# Vercel +# vercel .vercel -# Turborepo -.turbo - # typescript -*.tsbuildinfo \ No newline at end of file +*.tsbuildinfo +next-env.d.ts + +# misc +.DS_Store diff --git a/packages/sdk/vercel/examples/complete/.npmrc b/packages/sdk/vercel/examples/complete/.npmrc deleted file mode 100644 index e9ee3cb4d0..0000000000 --- a/packages/sdk/vercel/examples/complete/.npmrc +++ /dev/null @@ -1 +0,0 @@ -legacy-peer-deps=true \ No newline at end of file diff --git a/packages/sdk/vercel/examples/complete/README.md b/packages/sdk/vercel/examples/complete/README.md index 06a333c04b..42a5a92a78 100644 --- a/packages/sdk/vercel/examples/complete/README.md +++ b/packages/sdk/vercel/examples/complete/README.md @@ -1,58 +1,87 @@ -# Complete example app for Vercel LaunchDarkly SDK +# LaunchDarkly sample Vercel application -This example shows how to evaluate feature flags in Vercel's edge runtime using the [LaunchDarkly Vercel SDK](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/vercel). Two primary use cases are highlighted: +This example shows how to evaluate feature flags in Vercel's edge runtime using the +[LaunchDarkly Vercel SDK](https://github.com/launchdarkly/js-core/tree/main/packages/sdk/vercel). +Two primary use cases are highlighted: -1. Bootstrapping feature flags from the edge runtime and consuming them in the [LaunchDarkly Client-side SDK for React](https://github.com/launchdarkly/react-client-sdk). This is leveraging feature flags in edge-rendered pages while still maintaining the events and ergonomics provided by the React SDK. You can see details in [`app/layout.tsx`](./app/layout.tsx) and [`components/launchdarklyProvider.tsx`](./components/launchdarklyProvider.tsx). -2. Evaluating feature flags in the [Edge Middleware](https://vercel.com/docs/concepts/functions/edge-middleware). This can be seen in [`middleware.ts`](./middleware.ts). +1. **Edge Middleware** ([`proxy.ts`](./proxy.ts)) — evaluates a feature flag per request + and attaches the result as a header for server-rendering. +2. **Edge Route Handler** ([`app/api/flag/route.ts`](./app/api/flag/route.ts)) — evaluates a + feature flag and returns JSON, used by the client to poll for live updates. -## Demo - -https://hello-vercel-edge.vercel.app/ +Both share a single edge client defined in [`lib/ldEdgeClient.ts`](./lib/ldEdgeClient.ts). ## Local development -#### Create a new LaunchDarkly project and flags +#### Create a new LaunchDarkly project and flag -For simplicity, we recommend [creating a new LaunchDarkly project](https://docs.launchdarkly.com/home/organize/projects/?q=create+proj) for this example app. After creating a new project, create the following feature flags with Client-side SDK availability: +For simplicity, we recommend +[creating a new LaunchDarkly project](https://docs.launchdarkly.com/home/organize/projects/?q=create+proj) +for this example app. After creating a new project, create a single boolean feature flag with +client-side SDK availability: -- `bootstrap-flags` - (Boolean) - This flag will determine whether or not the LaunchDarkly React SDK will bootstrap feature flags from the edge. -- `show-debugging-info` - (Boolean) - This flag is used to expose the current flag values. -- `hero-text` - (String) - This flag is used to dynamically change the hero text. You can make the variations anything you want, e.g. "The best way to buy the products you love." -- `enable-hot-dog-favicon` - (Boolean) - This flag is used in middleware.ts to dynamically load a different favicon. -- `store-closed` - (Boolean) - This flag is evaluated in `middleware.ts` and can be used to load a different home page when the store is closed. +- `sample-feature` — (Boolean) the flag this example evaluates and renders. #### Set up the LaunchDarkly Vercel integration -You will need to have the LaunchDarkly Vercel integration configured to push feature flag data to your Vercel Edge Config. Read the [Vercel documentation](https://docs.launchdarkly.com/integrations/vercel/) to set up the integration. Be sure to connect the project you created above. +You will need to have the LaunchDarkly Vercel integration configured to push feature flag data to +your Vercel Edge Config. Read the +[Vercel documentation](https://docs.launchdarkly.com/integrations/vercel/) to set up the +integration. Be sure to connect the project you created above. #### Set up environment variables -1. Copy this directory in a new repository. +1. Copy this directory into a new repository. 2. Create a new Vercel project based on the new repository. -3. [Add a new environment variable to your project](https://vercel.com/docs/concepts/projects/environment-variables) named `LD_CLIENT_SIDE_ID` and set it to the LaunchDarkly client-side ID for the **Test** environment in the project you created above. -4. Follow [Vercel's documentation](https://vercel.com/docs/storage/edge-config/get-started) to connect an Edge Config to your new project. +3. [Add a new environment variable to your project](https://vercel.com/docs/concepts/projects/environment-variables) + named `LD_CLIENT_SIDE_ID` and set it to the LaunchDarkly client-side ID for the **Test** + environment in the project you created above. +4. Follow [Vercel's documentation](https://vercel.com/docs/storage/edge-config/get-started) to + connect an Edge Config to your new project. 5. Run the following command to link your local codebase to your Vercel project: -```shell -vercel link -``` + ```shell + vercel link + ``` -6. Run the following command to sync your projects environment variables in your development environment: +6. Run the following command to sync your project's environment variables in your development + environment: -```shell -vercel env pull .env.development.local -``` + ```shell + vercel env pull .env.development.local + ``` -7. After completing the guide above, you should have linked this example app to your Vercel project and created an `.env.development.local`. -8. Verify the contents of `.env.development.local` have values for the `LD_CLIENT_SIDE_ID` and `EDGE_CONFIG`. +7. After completing the steps above, you should have linked this example app to your Vercel + project and created a `.env.development.local`. +8. Verify the contents of `.env.development.local` have values for `LD_CLIENT_SIDE_ID` and + `EDGE_CONFIG`. 9. Run the following command to install all dependencies: -```shell -yarn -``` + ```shell + yarn + ``` 10. Run the following command to start your development environment: -```shell -yarn dev -``` + ```shell + yarn dev + ``` + +Open [http://localhost:3000](http://localhost:3000). You should see: + +- **Green background** (`#00844B`) when the flag evaluates to `true` +- **Dark background** (`#373841`) when the flag evaluates to `false` +- The message: "The sample-feature feature flag evaluates to true/false." + +The page polls `/api/flag` every 2 seconds. Toggle the flag in LaunchDarkly and the background +color will update automatically. + +## How it works + +| Path | Purpose | +|------|---------| +| [`proxy.ts`](./proxy.ts) | Edge Middleware that evaluates the flag and sets a header for server-rendering. | +| [`app/api/flag/route.ts`](./app/api/flag/route.ts) | Edge Route Handler that evaluates the flag and returns JSON for client polling. | +| [`app/page.tsx`](./app/page.tsx) | Server component that reads the middleware header for the initial render. | +| [`app/FlagDisplay.tsx`](./app/FlagDisplay.tsx) | Client component that polls `/api/flag` every 2 seconds for live updates. | +| [`lib/ldEdgeClient.ts`](./lib/ldEdgeClient.ts) | Lazily-initialized LaunchDarkly Vercel SDK client shared by middleware and route handler. | diff --git a/packages/sdk/vercel/examples/complete/app/FlagDisplay.tsx b/packages/sdk/vercel/examples/complete/app/FlagDisplay.tsx new file mode 100644 index 0000000000..08a1e20a98 --- /dev/null +++ b/packages/sdk/vercel/examples/complete/app/FlagDisplay.tsx @@ -0,0 +1,69 @@ +'use client'; + +import { useCallback, useEffect, useState } from 'react'; + +interface FlagState { + flagKey: string; + flagValue: boolean; +} + +export default function FlagDisplay({ initialState }: { initialState?: FlagState }) { + const [state, setState] = useState(initialState ?? null); + const [error, setError] = useState(null); + + const fetchFlag = useCallback(async () => { + try { + const res = await fetch('/api/flag'); + const data = await res.json(); + + if (data.error) { + setError(data.error); + return; + } + + // Only update state when the flag value actually changed to avoid + // unnecessary re-renders during polling. + setState((prev) => { + if (prev && prev.flagKey === data.flagKey && prev.flagValue === data.flagValue) { + return prev; + } + return { flagKey: data.flagKey, flagValue: data.flagValue }; + }); + setError(null); + } catch (e) { + setError(`Failed to fetch flag: ${e instanceof Error ? e.message : String(e)}`); + } + }, []); + + useEffect(() => { + fetchFlag(); + + // Poll every 2 seconds to pick up flag changes. + const interval = setInterval(fetchFlag, 2000); + return () => clearInterval(interval); + }, [fetchFlag]); + + if (error) { + return ( +
+

{error}

+
+ ); + } + + if (!state) { + return ( +
+

Initializing...

+
+ ); + } + + return ( +
+

+ The {state.flagKey} feature flag evaluates to {String(state.flagValue)}. +

+
+ ); +} diff --git a/packages/sdk/vercel/examples/complete/app/api/flag/route.ts b/packages/sdk/vercel/examples/complete/app/api/flag/route.ts new file mode 100644 index 0000000000..513bd7bbd9 --- /dev/null +++ b/packages/sdk/vercel/examples/complete/app/api/flag/route.ts @@ -0,0 +1,34 @@ +import { context, flagKey, getLdEdgeClient } from 'lib/ldEdgeClient'; + +export const runtime = 'edge'; + +export async function GET() { + const ldClient = getLdEdgeClient(); + if (!ldClient) { + return Response.json( + { + error: + 'LaunchDarkly is not configured: set the LD_CLIENT_SIDE_ID and EDGE_CONFIG environment variables and try again.', + }, + { status: 500 }, + ); + } + + try { + await ldClient.waitForInitialization(); + const flagValue = await ldClient.boolVariation(flagKey, context, false); + + return Response.json( + { flagKey, flagValue }, + { headers: { 'Cache-Control': 'no-store' } }, + ); + } catch { + return Response.json( + { + error: + 'SDK failed to initialize. Please check your Edge Config connection and LaunchDarkly client-side ID for any issues.', + }, + { status: 500 }, + ); + } +} diff --git a/packages/sdk/vercel/examples/complete/app/closed/page.tsx b/packages/sdk/vercel/examples/complete/app/closed/page.tsx deleted file mode 100644 index d8f6e833a5..0000000000 --- a/packages/sdk/vercel/examples/complete/app/closed/page.tsx +++ /dev/null @@ -1,23 +0,0 @@ -export default function Closed() { - return ( -
- - - -

- We'll be back. -

-

- We're busy updating the Apple Store for you and will be back soon. -

-
- ); -} diff --git a/packages/sdk/vercel/examples/complete/app/layout.tsx b/packages/sdk/vercel/examples/complete/app/layout.tsx index a5ce1a7431..e3006e4d8c 100644 --- a/packages/sdk/vercel/examples/complete/app/layout.tsx +++ b/packages/sdk/vercel/examples/complete/app/layout.tsx @@ -1,49 +1,13 @@ -import LaunchDarklyProvider from 'components/launchdarklyProvider'; -import Nav from 'components/nav'; -import { ldEdgeClient } from 'lib/ldEdgeClient'; -import { headers } from 'next/headers'; -import { ReactElement } from 'react'; -import 'tailwindcss/tailwind.css'; - -import { LDMultiKindContext } from '@launchdarkly/vercel-server-sdk'; - -// Specify the `edge` runtime to use the LaunchDarkly Edge SDK in layouts -export const runtime = 'edge'; - -export default async function RootLayout({ children }: { children: ReactElement }) { - const headersList = await headers(); - await ldEdgeClient.waitForInitialization(); - - // Here we are using basic information from the request as the LaunchDarkly context. If you have session auth in place, - // you will likely want to also include user and organization context. - const context: LDMultiKindContext = { - kind: 'multi', - user: { key: 'anonymous', anonymous: true }, - 'user-agent': { key: headersList.get('user-agent') || 'unknown' }, - method: { - key: 'GET', - }, - }; - - // The allFlagsState call is used to evaluate all feature flags for a given context so they can be bootstrapped but the - // LaunchDarkly React SDK in the `` component. - const allFlags = (await ldEdgeClient.allFlagsState(context)).toJSON() as { - 'bootstrap-flags': boolean; - }; - const bootstrappedFlags = allFlags['bootstrap-flags'] ? allFlags : undefined; +import './styles.css'; +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { return ( - - -