Skip to content

Commit 8181b9b

Browse files
Add Flags SDK LaunchDarkly example (#1126)
### Description Add Flags SDK LaunchDarkly example with deferred exposure tracking for experimentation --------- Co-authored-by: Aaron Morris <[email protected]>
1 parent 7fc3e94 commit 8181b9b

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+6357
-0
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# For @flags-sdk/launchdarkly
2+
FLAGS_SECRET=""
3+
EDGE_CONFIG=""
4+
LAUNCHDARKLY_PROJECT_SLUG=""
5+
LAUNCHDARKLY_CLIENT_SIDE_ID=""
6+
7+
# For launchdarkly-react-client-sdk
8+
NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_SIDE_ID=""
9+
10+
# For Flags Explorer
11+
LAUNCHDARKLY_API_KEY=""
12+
LAUNCHDARKLY_PROJECT_KEY=""
13+
LAUNCHDARKLY_ENVIRONMENT=""
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"root": true,
3+
"extends": "next/core-web-vitals",
4+
"rules": {
5+
"@typescript-eslint/require-await": "off",
6+
"@typescript-eslint/no-misused-promises": "off",
7+
"import/order": "off",
8+
"camelcase": "off",
9+
"no-console": "off"
10+
}
11+
}

flags-sdk/launchdarkly/.gitignore

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
.yarn/install-state.gz
8+
9+
# testing
10+
/coverage
11+
12+
# next.js
13+
/.next/
14+
/out/
15+
16+
# production
17+
/build
18+
19+
# misc
20+
.DS_Store
21+
*.pem
22+
23+
# debug
24+
npm-debug.log*
25+
yarn-debug.log*
26+
yarn-error.log*
27+
28+
# local env files
29+
.env*.local
30+
31+
# vercel
32+
.vercel
33+
34+
# typescript
35+
*.tsbuildinfo
36+
next-env.d.ts

flags-sdk/launchdarkly/README.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# LaunchDarkly Flags SDK Example
2+
3+
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).
4+
5+
## Demo
6+
7+
TODO
8+
9+
## How it works
10+
11+
This demo uses two feature flags on LaunchDarkly to control the visibility of two banners on the page.
12+
Both flags are configured to show/hide each banner 50% of the time.
13+
14+
Once you visit the page, you can see a variation of both/one/none of the banners.
15+
Since this example is using a stable id to identify users, you will see the same variation until you reset your id.
16+
17+
To test different variations, you can use the Dev Tools at the bottom to reset the stable id and reload the page.
18+
This allows you to test different variations of the banners.
19+
20+
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.
21+
22+
## Deploy this template
23+
24+
The easiest way to get started with LaunchDarkly is through the integration in [Vercel Marketplace](https://vercel.com/marketplace/launchdarkly).
25+
26+
[![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)
27+
28+
### Step 1: Link the project
29+
30+
In order to use the Flags Explorer, you need to link the project on your local machine.
31+
32+
```bash
33+
vercel link
34+
```
35+
36+
Select the project from the list you just deployed.
37+
38+
### Step 2: Pull all environment variables
39+
40+
This allows the Flags SDK and the Flags Explorer to work correctly, by getting additional metadata.
41+
42+
```bash
43+
vercel env pull
44+
```
45+
46+
### Step 3: Create Feature Flags
47+
48+
Head over to the [LaunchDarkly Console](https://app.launchdarkly.com) and create the feature flags and experiments required by this template.
49+
50+
Be sure to enable the `SDKs using Client-side ID` option for each Feature Flag.
51+
52+
Feature Flags:
53+
54+
- `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`.
55+
- `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`.
56+
- `Proceed to Checkout` (type string) with the key `proceed-to-checkout` and the following variations:
57+
- Name: `Control`, Value: `blue`
58+
- Name: `Test`, Value: `green`
59+
- Name: `Test #2`, Value: `red`
60+
61+
You can also find the flag keys in the `flags.ts` file.
62+
63+
Ensure all Flags are ON.
64+
65+
### Step 4: Configure the Experiment
66+
67+
Create the `Proceed to Checkout` experiment:
68+
69+
- Name: `Proceed to Checkout`
70+
- Hypothesis: `Button color influences rate at which customers proceed to checkout`
71+
- Type: `Feature change`
72+
- Randomization Unit: `user`
73+
- Randomization Attribute: `key`
74+
- Metric: Create a new metric:
75+
- Event kind: `Custom`
76+
- Event key: `proceed-to-checkout-clicked`
77+
- What do you want to measure? `Occurence (conversion: binary)`
78+
- Metric definition: `Percentage of user units that sent the event, where higher is better`
79+
- Metric name: `Proceed to Checkout Clicked`
80+
- Varaiations: Choose flag `proceed-to-checkout`
81+
- Audience:
82+
- In this experiment: Choose `Custom` and enter `100`%
83+
- Split audience: Choose `Split equally`
84+
- Statistical approach: Default values
85+
86+
After that, start the Experiment.
87+
88+
### Step 6: Set environment variables
89+
90+
See `.env.example` for a template.
91+
92+
- [`FLAGS_SECRET`](https://vercel.com/docs/feature-flags/flags-explorer/reference#flags_secret-environment-variable)
93+
- `EDGE_CONFIG` (Vercel Edge Config connection string)
94+
- `LAUNCHDARKLY_PROJECT_SLUG`
95+
- `LAUNCHDARKLY_CLIENT_SIDE_ID`
96+
- `NEXT_PUBLIC_LAUNCHDARKLY_CLIENT_SIDE_ID` (set to same value as `LAUNCHDARKLY_CLIENT_SIDE_ID`)
97+
98+
_(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.
99+
100+
This will show the description (if set) and displays a link to the feature flag on the LaunchDarkly Console.
101+
102+
You can create an API key and find project and environment values in the [LaunchDarkly Console](https://app.launchdarkly.com/settings/projects).
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { mergeProviderData } from 'flags';
2+
import { getProviderData, createFlagsDiscoveryEndpoint } from 'flags/next';
3+
import { getProviderData as getLDProviderData } from '@flags-sdk/launchdarkly';
4+
import * as flags from '../../../../flags';
5+
6+
export const GET = createFlagsDiscoveryEndpoint(async (request) => {
7+
const otherData = getProviderData(flags);
8+
if (
9+
!process.env.LAUNCHDARKLY_API_KEY ||
10+
!process.env.LAUNCHDARKLY_PROJECT_KEY ||
11+
!process.env.LAUNCHDARKLY_ENVIRONMENT
12+
) {
13+
return otherData;
14+
}
15+
const ldData = await getLDProviderData({
16+
apiKey: process.env.LAUNCHDARKLY_API_KEY,
17+
projectKey: process.env.LAUNCHDARKLY_PROJECT_KEY,
18+
environment: process.env.LAUNCHDARKLY_ENVIRONMENT,
19+
});
20+
return mergeProviderData([otherData, ldData]);
21+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useRouter } from 'next/navigation'
5+
import { addToCart } from '@/lib/actions'
6+
import { useProductDetailPageContext } from '@/components/utils/product-detail-page-context'
7+
import { AddToCartButton } from '@/components/product-detail-page/add-to-cart-button'
8+
9+
export function AddToCart() {
10+
const router = useRouter()
11+
const { color, size } = useProductDetailPageContext()
12+
const [isLoading, setIsLoading] = useState(false)
13+
14+
return (
15+
<AddToCartButton
16+
isLoading={isLoading}
17+
onClick={async () => {
18+
setIsLoading(true)
19+
await addToCart({ id: 'shirt', color, size, quantity: 1 })
20+
router.push('/cart')
21+
}}
22+
/>
23+
)
24+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { proceedToCheckoutColorFlag } from '@/flags'
2+
import { OrderSummarySection } from '@/components/shopping-cart/order-summary-section'
3+
import { ProceedToCheckout } from './proceed-to-checkout'
4+
5+
export async function OrderSummary({
6+
showSummerBanner,
7+
freeDelivery,
8+
proceedToCheckoutColor,
9+
}: {
10+
showSummerBanner: boolean;
11+
freeDelivery: boolean;
12+
proceedToCheckoutColor: string;
13+
}) {
14+
return (
15+
<OrderSummarySection
16+
showSummerBanner={showSummerBanner}
17+
freeDelivery={freeDelivery}
18+
proceedToCheckout={
19+
<ProceedToCheckout
20+
color={proceedToCheckoutColor}
21+
flagKey={proceedToCheckoutColorFlag.key}
22+
/>
23+
}
24+
/>
25+
);
26+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { OrderSummary } from '@/app/[code]/cart/order-summary'
2+
import { Main } from '@/components/main'
3+
import { ShoppingCart } from '@/components/shopping-cart/shopping-cart'
4+
import {
5+
productFlags,
6+
showFreeDeliveryBannerFlag,
7+
showSummerBannerFlag,
8+
proceedToCheckoutColorFlag,
9+
} from '@/flags'
10+
11+
export default async function CartPage({
12+
params,
13+
}: {
14+
params: Promise<{ code: string }>
15+
}) {
16+
const { code } = await params;
17+
const showSummerBanner = await showSummerBannerFlag(
18+
code,
19+
productFlags,
20+
);
21+
const freeDeliveryBanner = await showFreeDeliveryBannerFlag(
22+
code,
23+
productFlags,
24+
);
25+
const proceedToCheckoutColor = await proceedToCheckoutColorFlag(
26+
code,
27+
productFlags,
28+
);
29+
30+
return (
31+
<Main>
32+
<div className="lg:grid lg:grid-cols-12 lg:items-start lg:gap-x-12 xl:gap-x-16">
33+
<ShoppingCart />
34+
<OrderSummary
35+
showSummerBanner={showSummerBanner}
36+
freeDelivery={freeDeliveryBanner}
37+
proceedToCheckoutColor={proceedToCheckoutColor}
38+
/>
39+
</div>
40+
</Main>
41+
)
42+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
'use client'
2+
3+
import { ProceedToCheckoutButton } from '@/components/shopping-cart/proceed-to-checkout-button';
4+
import { toast } from 'sonner';
5+
import { useLDClient } from 'launchdarkly-react-client-sdk';
6+
import { useLDFlagExposure, trackLDEvent } from '@/launchdarkly/launchdarkly-flag-exposure';
7+
8+
export function ProceedToCheckout({
9+
color,
10+
flagKey,
11+
}: {
12+
color: string;
13+
flagKey: string;
14+
}) {
15+
const ldClient = useLDClient();
16+
useLDFlagExposure(flagKey, ldClient);
17+
return (
18+
<>
19+
<ProceedToCheckoutButton
20+
color={color}
21+
onClick={() => {
22+
trackLDEvent('proceed-to-checkout-clicked', ldClient);
23+
toast('End reached', {
24+
className: 'my-classname',
25+
description:
26+
'The checkout flow is not implemented in this template.',
27+
duration: 5000,
28+
});
29+
}}
30+
/>
31+
</>
32+
);
33+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { StaticLDProvider } from '@/launchdarkly/launchdarkly-provider'
2+
import { deserialize, generatePermutations } from 'flags/next'
3+
import { FlagValues } from 'flags/react'
4+
import { productFlags, showFreeDeliveryBannerFlag } from '@/flags'
5+
import { FreeDelivery } from '@/app/free-delivery'
6+
import { DevTools } from '@/components/dev-tools'
7+
import { Footer } from '@/components/footer'
8+
import { Navigation } from '@/components/navigation'
9+
10+
export async function generateStaticParams() {
11+
// Returning an empty array here is important as it enables ISR, so
12+
// the various combinations stay cached after they first time they were rendered.
13+
//
14+
// return [];
15+
16+
// Instead of returning an empty array you could also call generatePermutations
17+
// to generate the permutations upfront.
18+
const codes = await generatePermutations(productFlags);
19+
return codes.map((code) => ({ code }));
20+
}
21+
22+
export default async function Layout(props: {
23+
children: React.ReactNode
24+
params: Promise<{
25+
code: string
26+
}>
27+
}) {
28+
const params = await props.params;
29+
const values = await deserialize(productFlags, params.code);
30+
31+
const showFreeDeliveryBanner = await showFreeDeliveryBannerFlag(
32+
params.code,
33+
productFlags,
34+
);
35+
36+
return (
37+
<StaticLDProvider>
38+
<div className="bg-white">
39+
<FreeDelivery
40+
show={showFreeDeliveryBanner}
41+
flagKey={showFreeDeliveryBannerFlag.key}
42+
/>
43+
<Navigation />
44+
{props.children}
45+
<FlagValues values={values} />
46+
<Footer />
47+
<DevTools />
48+
</div>
49+
</StaticLDProvider>
50+
);
51+
}

0 commit comments

Comments
 (0)