Skip to content

Add Flags SDK LaunchDarkly example #1126

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

Merged
Merged
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
13 changes: 13 additions & 0 deletions flags-sdk/launchdarkly/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# For @flags-sdk/launchdarkly
FLAGS_SECRET=""
EDGE_CONFIG=""
LAUNCHDARKLY_PROJECT_SLUG=""
LAUNCHDARKLY_CLIENT_SIDE_ID=""

# For launchdarkly-react-client-sdk
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_SIDE_ID=""

# For Flags Explorer
LAUNCHDARKLY_API_KEY=""
LAUNCHDARKLY_PROJECT_KEY=""
LAUNCHDARKLY_ENVIRONMENT=""
11 changes: 11 additions & 0 deletions flags-sdk/launchdarkly/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"root": true,
"extends": "next/core-web-vitals",
"rules": {
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-misused-promises": "off",
"import/order": "off",
"camelcase": "off",
"no-console": "off"
}
}
36 changes: 36 additions & 0 deletions flags-sdk/launchdarkly/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
102 changes: 102 additions & 0 deletions flags-sdk/launchdarkly/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# LaunchDarkly Flags SDK Example

This example uses [LaunchDarkly](https://vercel.com/marketplace/launchdarkly) for feature flags with the [Flags SDK](https://flags-sdk.dev) along with the `@flags-sdk/launchdarkly` [LaunchDarkly adapter](https://flags-sdk.dev/docs/api-reference/adapters/launchdarkly) and the [Flags Explorer](https://vercel.com/docs/workflow-collaboration/feature-flags/using-vercel-toolbar).

## Demo

TODO

## How it works

This demo uses two feature flags on LaunchDarkly to control the visibility of two banners on the page.
Both flags are configured to show/hide each banner 50% of the time.

Once you visit the page, you can see a variation of both/one/none of the banners.
Since this example is using a stable id to identify users, you will see the same variation until you reset your id.

To test different variations, you can use the Dev Tools at the bottom to reset the stable id and reload the page.
This allows you to test different variations of the banners.

If you deployed your own and configured the feature flags on LaunchDarkly, you can also use the [Flags Explorer](https://vercel.com/docs/workflow-collaboration/feature-flags/using-vercel-toolbar) to test different variations by creating overrides.

## Deploy this template

The easiest way to get started with LaunchDarkly is through the integration in [Vercel Marketplace](https://vercel.com/marketplace/launchdarkly).

[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fvercel%2Fexamples%2Ftree%2Fmain%2Fflags-sdk%2Flaunchdarkly&env=FLAGS_SECRET&envDescription=The+FLAGS_SECRET+will+be+used+by+the+Flags+Explorer+to+securely+overwrite+feature+flags.+Must+be+32+random+bytes%2C+base64-encoded.+Use+the+generated+value+or+set+your+own.&envLink=https%3A%2F%2Fvercel.com%2Fdocs%2Fworkflow-collaboration%2Ffeature-flags%2Fsupporting-feature-flags%23flags_secret-environment-variable&project-name=launchdarkly-flags-sdk&repository-name=launchdarkly-flags-sdk)

### Step 1: Link the project

In order to use the Flags Explorer, you need to link the project on your local machine.

```bash
vercel link
```

Select the project from the list you just deployed.

### Step 2: Pull all environment variables

This allows the Flags SDK and the Flags Explorer to work correctly, by getting additional metadata.

```bash
vercel env pull
```

### Step 3: Create Feature Flags

Head over to the [LaunchDarkly Console](https://app.launchdarkly.com) and create the feature flags and experiments required by this template.

Be sure to enable the `SDKs using Client-side ID` option for each Feature Flag.

Feature Flags:

- `Summer Sale` (type boolean) with the key `summer-sale` and the variations `true` and `false`. Edit the default targeting rule to serve a percentage rollout with a 50/50 split by `user.key`.
- `Free Delivery` (type boolean) with the key `free-delivery` and the variations `true` and `false`. Edit the default targeting rule to serve a percentage rollout with a 50/50 split by `user.key`.
- `Proceed to Checkout` (type string) with the key `proceed-to-checkout` and the following variations:
- Name: `Control`, Value: `blue`
- Name: `Test`, Value: `green`
- Name: `Test #2`, Value: `red`

You can also find the flag keys in the `flags.ts` file.

Ensure all Flags are ON.

### Step 4: Configure the Experiment

Create the `Proceed to Checkout` experiment:

- Name: `Proceed to Checkout`
- Hypothesis: `Button color influences rate at which customers proceed to checkout`
- Type: `Feature change`
- Randomization Unit: `user`
- Randomization Attribute: `key`
- Metric: Create a new metric:
- Event kind: `Custom`
- Event key: `proceed-to-checkout-clicked`
- What do you want to measure? `Occurence (conversion: binary)`
- Metric definition: `Percentage of user units that sent the event, where higher is better`
- Metric name: `Proceed to Checkout Clicked`
- Varaiations: Choose flag `proceed-to-checkout`
- Audience:
- In this experiment: Choose `Custom` and enter `100`%
- Split audience: Choose `Split equally`
- Statistical approach: Default values

After that, start the Experiment.

### Step 6: Set environment variables

See `.env.example` for a template.

- [`FLAGS_SECRET`](https://vercel.com/docs/feature-flags/flags-explorer/reference#flags_secret-environment-variable)
- `EDGE_CONFIG` (Vercel Edge Config connection string)
- `LAUNCHDARKLY_PROJECT_SLUG`
- `LAUNCHDARKLY_CLIENT_SIDE_ID`
- `NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_SIDE_ID` (set to same value as `LAUNCHDARKLY_CLIENT_SIDE_ID`)

_(Optional)_ If you provide the `LAUNCHDARKLY_API_KEY`, `LAUNCHDARKLY_PROJECT_KEY` and `LAUNCHDARKLY_ENVIRONMENT` environment variables, the Flags Explorer will fetch additional metadata from the LaunchDarkly API.

This will show the description (if set) and displays a link to the feature flag on the LaunchDarkly Console.

You can create an API key and find project and environment values in the [LaunchDarkly Console](https://app.launchdarkly.com/settings/projects).
21 changes: 21 additions & 0 deletions flags-sdk/launchdarkly/app/.well-known/vercel/flags/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { mergeProviderData } from 'flags';
import { getProviderData, createFlagsDiscoveryEndpoint } from 'flags/next';
import { getProviderData as getLDProviderData } from '@flags-sdk/launchdarkly';
import * as flags from '../../../../flags';

export const GET = createFlagsDiscoveryEndpoint(async (request) => {
const otherData = getProviderData(flags);
if (
!process.env.LAUNCHDARKLY_API_KEY ||
!process.env.LAUNCHDARKLY_PROJECT_KEY ||
!process.env.LAUNCHDARKLY_ENVIRONMENT
) {
return otherData;
}
const ldData = await getLDProviderData({
apiKey: process.env.LAUNCHDARKLY_API_KEY,
projectKey: process.env.LAUNCHDARKLY_PROJECT_KEY,
environment: process.env.LAUNCHDARKLY_ENVIRONMENT,
});
return mergeProviderData([otherData, ldData]);
});
24 changes: 24 additions & 0 deletions flags-sdk/launchdarkly/app/[code]/add-to-cart.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { addToCart } from '@/lib/actions'
import { useProductDetailPageContext } from '@/components/utils/product-detail-page-context'
import { AddToCartButton } from '@/components/product-detail-page/add-to-cart-button'

export function AddToCart() {
const router = useRouter()
const { color, size } = useProductDetailPageContext()
const [isLoading, setIsLoading] = useState(false)

return (
<AddToCartButton
isLoading={isLoading}
onClick={async () => {
setIsLoading(true)
await addToCart({ id: 'shirt', color, size, quantity: 1 })
router.push('/cart')
}}
/>
)
}
26 changes: 26 additions & 0 deletions flags-sdk/launchdarkly/app/[code]/cart/order-summary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { proceedToCheckoutColorFlag } from '@/flags'
import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'
import { ProceedToCheckout } from './proceed-to-checkout'

export async function OrderSummary({
showSummerBanner,
freeDelivery,
proceedToCheckoutColor,
}: {
showSummerBanner: boolean;
freeDelivery: boolean;
proceedToCheckoutColor: string;
}) {
return (
<OrderSummarySection
showSummerBanner={showSummerBanner}
freeDelivery={freeDelivery}
proceedToCheckout={
<ProceedToCheckout
color={proceedToCheckoutColor}
flagKey={proceedToCheckoutColorFlag.key}
/>
}
/>
);
}
42 changes: 42 additions & 0 deletions flags-sdk/launchdarkly/app/[code]/cart/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { OrderSummary } from '@/app/[code]/cart/order-summary'
import { Main } from '@/components/main'
import { ShoppingCart } from '@/components/shopping-cart/shopping-cart'
import {
productFlags,
showFreeDeliveryBannerFlag,
showSummerBannerFlag,
proceedToCheckoutColorFlag,
} from '@/flags'

export default async function CartPage({
params,
}: {
params: Promise<{ code: string }>
}) {
const { code } = await params;
const showSummerBanner = await showSummerBannerFlag(
code,
productFlags,
);
const freeDeliveryBanner = await showFreeDeliveryBannerFlag(
code,
productFlags,
);
const proceedToCheckoutColor = await proceedToCheckoutColorFlag(
code,
productFlags,
);

return (
<Main>
<div className="lg:grid lg:grid-cols-12 lg:items-start lg:gap-x-12 xl:gap-x-16">
<ShoppingCart />
<OrderSummary
showSummerBanner={showSummerBanner}
freeDelivery={freeDeliveryBanner}
proceedToCheckoutColor={proceedToCheckoutColor}
/>
</div>
</Main>
)
}
33 changes: 33 additions & 0 deletions flags-sdk/launchdarkly/app/[code]/cart/proceed-to-checkout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use client'

import { ProceedToCheckoutButton } from '@/components/shopping-cart/proceed-to-checkout-button';
import { toast } from 'sonner';
import { useLDClient } from 'launchdarkly-react-client-sdk';
import { useLDFlagExposure, trackLDEvent } from '@/launchdarkly/launchdarkly-flag-exposure';

export function ProceedToCheckout({
color,
flagKey,
}: {
color: string;
flagKey: string;
}) {
const ldClient = useLDClient();
useLDFlagExposure(flagKey, ldClient);
return (
<>
<ProceedToCheckoutButton
color={color}
onClick={() => {
trackLDEvent('proceed-to-checkout-clicked', ldClient);
toast('End reached', {
className: 'my-classname',
description:
'The checkout flow is not implemented in this template.',
duration: 5000,
});
}}
/>
</>
);
}
51 changes: 51 additions & 0 deletions flags-sdk/launchdarkly/app/[code]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { StaticLDProvider } from '@/launchdarkly/launchdarkly-provider'
import { deserialize, generatePermutations } from 'flags/next'
import { FlagValues } from 'flags/react'
import { productFlags, showFreeDeliveryBannerFlag } from '@/flags'
import { FreeDelivery } from '@/app/free-delivery'
import { DevTools } from '@/components/dev-tools'
import { Footer } from '@/components/footer'
import { Navigation } from '@/components/navigation'

export async function generateStaticParams() {
// Returning an empty array here is important as it enables ISR, so
// the various combinations stay cached after they first time they were rendered.
//
// return [];

// Instead of returning an empty array you could also call generatePermutations
// to generate the permutations upfront.
const codes = await generatePermutations(productFlags);
return codes.map((code) => ({ code }));
}

export default async function Layout(props: {
children: React.ReactNode
params: Promise<{
code: string
}>
}) {
const params = await props.params;
const values = await deserialize(productFlags, params.code);

const showFreeDeliveryBanner = await showFreeDeliveryBannerFlag(
params.code,
productFlags,
);

return (
<StaticLDProvider>
<div className="bg-white">
<FreeDelivery
show={showFreeDeliveryBanner}
flagKey={showFreeDeliveryBannerFlag.key}
/>
<Navigation />
{props.children}
<FlagValues values={values} />
<Footer />
<DevTools />
</div>
</StaticLDProvider>
);
}
Loading