Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/react/examples/vercel-edge/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@internal/react-sdk-example-vercel-edge",
"name": "@launchdarkly/react-sdk-example-vercel-edge",
"private": true,
"scripts": {
"dev": "next dev",
Expand Down
3 changes: 3 additions & 0 deletions packages/sdk/vercel/examples/complete/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
LD_CLIENT_SIDE_ID=
EDGE_CONFIG=
LAUNCHDARKLY_FLAG_KEY=sample-feature
3 changes: 0 additions & 3 deletions packages/sdk/vercel/examples/complete/.eslintrc.json

This file was deleted.

45 changes: 11 additions & 34 deletions packages/sdk/vercel/examples/complete/.gitignore
Original file line number Diff line number Diff line change
@@ -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
*.tsbuildinfo
next-env.d.ts

# misc
.DS_Store
1 change: 0 additions & 1 deletion packages/sdk/vercel/examples/complete/.npmrc

This file was deleted.

95 changes: 62 additions & 33 deletions packages/sdk/vercel/examples/complete/README.md
Original file line number Diff line number Diff line change
@@ -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. |
69 changes: 69 additions & 0 deletions packages/sdk/vercel/examples/complete/app/FlagDisplay.tsx
Original file line number Diff line number Diff line change
@@ -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<FlagState | null>(initialState ?? null);
const [error, setError] = useState<string | null>(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 (
<div className="app app--off">
<p>{error}</p>
</div>
);
}

if (!state) {
return (
<div className="app app--off">
<p>Initializing...</p>
</div>
);
}

return (
<div className={`app ${state.flagValue ? 'app--on' : 'app--off'}`}>
<p>
The {state.flagKey} feature flag evaluates to {String(state.flagValue)}.
</p>
</div>
);
}
34 changes: 34 additions & 0 deletions packages/sdk/vercel/examples/complete/app/api/flag/route.ts
Original file line number Diff line number Diff line change
@@ -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 },
);
}
}
23 changes: 0 additions & 23 deletions packages/sdk/vercel/examples/complete/app/closed/page.tsx

This file was deleted.

50 changes: 7 additions & 43 deletions packages/sdk/vercel/examples/complete/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 `<LaunchDarklyProvider>` 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 (
<html lang="en">
<body>
<LaunchDarklyProvider
envId={process.env.LD_CLIENT_SIDE_ID || ''}
context={context}
bootstrappedFlags={bootstrappedFlags}
>
<Nav />
{children}
</LaunchDarklyProvider>
</body>
<body>{children}</body>
</html>
);
}
Loading
Loading