Skip to content
Open
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
77 changes: 77 additions & 0 deletions .github/workflows/react-native-bundle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: React Native example — Bundle

# Bundles the examples/React-Native app with Metro for BOTH platforms. This is a
# JS-only export (no native build), so it runs on Linux and catches
# module-resolution, transform, and Expo-config errors before they reach a device.

on:
pull_request:
paths:
- 'examples/React-Native/**'
- '.github/workflows/react-native-bundle.yml'
push:
branches: [main]
paths:
- 'examples/React-Native/**'
- '.github/workflows/react-native-bundle.yml'
# Weekly canary: the LaunchDarkly SDKs use the floating `latest` tag, so a new
# SDK major can land on a fresh install without a package.json change. This
# re-resolves them and bundles, catching a breaking major proactively.
schedule:
- cron: '0 12 * * 1' # Mondays 12:00 UTC
workflow_dispatch:

defaults:
run:
working-directory: examples/React-Native

jobs:
bundle:
name: Bundle (${{ matrix.platform }})
# Runs on PRs, pushes, and manual dispatch. Manual runs bundle the locked
# dependencies; the canary (below) re-resolves `latest` and runs on schedule.
if: github.event_name != 'schedule'
runs-on: ubuntu-latest
strategy:
fail-fast: false # both platforms must be reported even if one fails
matrix:
platform: [android, ios]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable Corepack
run: corepack enable
- name: Install dependencies
run: yarn install --immutable
- name: Provide placeholder env
# react-native-dotenv runs with safe:true, so MOBILE_KEY must exist at
# build time. .env is gitignored; a dummy key is fine for a static bundle.
run: echo "MOBILE_KEY=mob-00000000-0000-0000-0000-000000000000" > .env
- name: Bundle ${{ matrix.platform }}
run: yarn expo export --platform ${{ matrix.platform }} --output-dir dist-${{ matrix.platform }}

latest-canary:
name: Latest canary (${{ matrix.platform }})
if: github.event_name == 'schedule'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: [android, ios]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Enable Corepack
run: corepack enable
- name: Re-resolve the floating `latest` LaunchDarkly SDKs
run: yarn up @launchdarkly/react-native-client-sdk @launchdarkly/observability-react-native
- name: Install dependencies
run: yarn install --immutable
- name: Provide placeholder env
run: echo "MOBILE_KEY=mob-00000000-0000-0000-0000-000000000000" > .env
Comment thread
professorice marked this conversation as resolved.
- name: Bundle ${{ matrix.platform }}
run: yarn expo export --platform ${{ matrix.platform }} --output-dir dist-${{ matrix.platform }}
96 changes: 83 additions & 13 deletions examples/React-Native/App.tsx
Original file line number Diff line number Diff line change
@@ -1,51 +1,121 @@
import { useEffect, useState } from 'react';
import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
import { MOBILE_KEY } from '@env';
import {
AutoEnvAttributes,
LDProvider,
ReactNativeLDClient,
LDOptions,
} from '@launchdarkly/react-native-client-sdk';
import Welcome from './src/welcome';
import { Observability } from '@launchdarkly/observability-react-native';
import SignupFunnel from './src/SignupFunnel';

// In an Expo app, read the real build version from `expo-application` /
// `expo-updates` and pass it as `serviceVersion`. We use a static value here.
// https://launchdarkly.com/docs/sdk/observability/react-native#report-version-information-in-expo-apps
const SERVICE_VERSION = '1.0.0';

const options: LDOptions = {
// Optional: Configure application metadata for engineering insights.
// Optional: application metadata for engineering insights.
// https://launchdarkly.com/docs/home/releases/applications
applicationInfo: {
id: 'Hello-React-Native-Expo',
name: 'Sample Application',
version: '1.0.0',
version: SERVICE_VERSION,
versionName: 'v1',
},
Comment thread
professorice marked this conversation as resolved.
debug: true,
}
plugins: [
// LaunchDarkly Observability plugin. A few common options are shown; all are
// optional and the plugin works with a bare `new Observability()`. For the
// full list of options, see:
// https://launchdarkly.com/docs/sdk/observability/react-native
// https://launchdarkly.com/docs/sdk/features/observability-config-client-side#react-native
new Observability({
// Identifies this app in traces, logs, and metrics.
serviceName: 'hello-react-native-expo',
// Recommended: set to the latest deployed git SHA or app version.
serviceVersion: SERVICE_VERSION,

// Extra resource attributes attached to all telemetry.
resourceAttributes: {
'app.tier': 'sample',
'deploy.environment': 'development',
},

// Give contexts a friendly display name in the LaunchDarkly UI.
contextFriendlyName: (context) =>
'key' in context ? String(context.key) : undefined,

// Endpoint overrides — only needed if you proxy telemetry through your own
// domain (see the "Using a proxy" docs). Defaults shown for reference.
// backendUrl: 'https://pub.observability.app.launchdarkly.com',
// otlpEndpoint: 'https://otel.observability.app.launchdarkly.com:4318',
}),
],
};

const userContext = { kind: 'user', key: 'test-hello' };

export default function App() {
const [client, setClient] = useState<ReactNativeLDClient | null>(null);

useEffect(() => {
let cancelled = false;

// Initialize client
const featureClient = new ReactNativeLDClient(MOBILE_KEY, AutoEnvAttributes.Enabled, options);

featureClient.identify(userContext).catch((e: any) => console.log(e));

setClient(featureClient);

const featureClient = new ReactNativeLDClient(
MOBILE_KEY,
AutoEnvAttributes.Enabled,
options,
);

// Render once identify() settles. On success, flags evaluate against the
// intended context (and record exposures correctly) rather than returning
// per-call defaults during startup. On failure we still render, so the demo
// isn't stuck on a blank screen when LaunchDarkly is unreachable or the
// mobile key is misconfigured — flags fall back to their per-call defaults.
// The `cancelled` guard avoids a state update if the component unmounts
// before identify() settles.
featureClient
.identify(userContext)
.catch((e: any) => console.error(e))
.finally(() => {
if (!cancelled) {
setClient(featureClient);
}
});

// Cleanup function that runs when component unmounts
return () => {
cancelled = true;
featureClient.close();
};
}, []);

if (!client) {
return null;
// Minimal loading state while the SDK initializes, so the screen isn't blank.
return (
<View style={styles.loading}>
<ActivityIndicator size="large" color="#405BFF" />
<Text style={styles.loadingText}>Connecting to LaunchDarkly…</Text>
</View>
);
}

return (
<LDProvider client={client}>
<Welcome />
<SignupFunnel />
</LDProvider>
);
Comment thread
professorice marked this conversation as resolved.
}

const styles = StyleSheet.create({
loading: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#fff',
},
loadingText: { marginTop: 12, color: '#555' },
});
114 changes: 105 additions & 9 deletions examples/React-Native/README.md
Original file line number Diff line number Diff line change
@@ -1,21 +1,117 @@
# LaunchDarkly sample React Native application

We've built a simple mobile application that demonstrates how LaunchDarkly's SDK works.
This sample app demonstrates three LaunchDarkly capabilities in one React Native (Expo) app:

Below, you'll find the build procedure. For more comprehensive instructions, you can visit your [Quickstart page](https://app.launchdarkly.com/quickstart#/) or the [React Native reference guide](https://docs.launchdarkly.com/sdk/client-side/react/react-native).
- **Feature flags & experimentation** — a multi-step sign-up "funnel" whose UI is controlled by a flag, so it can be run as an A/B experiment.
- **Observability** — the LaunchDarkly Observability plugin, initialized in [`App.tsx`](./App.tsx) with a few common options documented. See the [observability config docs](https://launchdarkly.com/docs/sdk/features/observability-config-client-side#react-native) for the full list.
- **Metrics / event tracking** — each funnel step sends a custom event with `ldClient.track()`, which powers both the experiment's funnel metric group and the Observability product-analytics funnels.

This demo requires Node 18 or higher LTS version, Android Studio with an emulated device running API 34 and Watchman (for Linux or macOS users).
For more comprehensive instructions, visit your [Quickstart page](https://app.launchdarkly.com/quickstart#/) or the [React Native reference guide](https://launchdarkly.com/docs/sdk/client-side/react/react-native).

This demo requires Node 22 or higher LTS, Android Studio with an emulated device running API 34, and Watchman (for Linux or macOS users).

## Build instructions

1. Create an `.env` file and set the value of `MOBILE_KEY` to your LaunchDarkly mobile key. If there is an existing boolean feature flag in your LaunchDarkly project that you want to evaluate, set my-boolean-flag to the flag key.
1. Create an `.env` file and set `MOBILE_KEY` to your LaunchDarkly environment's mobile key:

```
MOBILE_KEY=your-mobile-key
```

2. On the command line, run `yarn` to install dependencies.

3. Run the app. **Observability requires a development build, not Expo Go**, because the Observability plugin relies on native modules that do not run in Expo Go. Build and launch a dev client:

```bash
# iOS: compile and run a dev build
npx expo run:ios

# Android: compile and run a dev build
npx expo run:android

# or, if a dev build is already installed, just start the bundler:
npx expo start --dev-client
```

Feature flags and experimentation event tracking also work in plain Expo Go (`yarn ios` / `yarn android`); only the native observability data collection requires the dev build above.

To start fresh, run `yarn clean-reset`. To reset Watchman, run `yarn watchman-reset`.

> **Note on SDK versions:** `@launchdarkly/react-native-client-sdk` and `@launchdarkly/observability-react-native` use the `latest` dist-tag so this sample always demonstrates the newest release. The tradeoff is that a fresh install can pick up a new major version. The committed `yarn.lock` keeps installs reproducible until it is refreshed, and CI runs a weekly canary that re-resolves `latest` and re-bundles both platforms to catch breaking changes early. If you need fully pinned, reproducible installs, replace `latest` with a specific version range.
>
> Note that `yarn clean-reset` deletes `yarn.lock`, so the next install re-resolves the `latest` SDKs and opts out of that reproducibility. Use plain `yarn install` if you want to keep the committed lockfile.

## What the app does

The app renders a three-step sign-up funnel:

1. **Sign up** button → sends the `signup-button-clicked` event.
2. **Choose a plan** and continue → sends the `signup-info-entered` event.
3. **Complete sign-up** → sends the `signup-completed` event, then flushes events.

The `signup-flow-variation` flag decides how the "choose a plan" step is rendered (`control`, `dropdown`, or `radio`), which is the change under experiment.

---

## LaunchDarkly UI setup (required to see results)

Complete the steps below in the LaunchDarkly UI so the app's flag, events, and experiment produce results. This mirrors the [funnel optimization guide](https://launchdarkly.com/docs/guides/experimentation/funnel-optimization) and the [React Native experimentation docs](https://launchdarkly.com/docs/sdk/features/experimentation#react-native).

### 1. Create the flag

Create a flag that selects the sign-up UI variation:

- **Create → Flag** (optionally choose **Custom → Experiment**, which makes the flag a **String** type).
- **Key:** `signup-flow-variation` (must match the code).
- Add **three string variations**, using these exact **values** (the code compares against them):

| Variation name | Value |
| --- | --- |
| Control | `control` |
| Dropdown select | `dropdown` |
| Radio select | `radio` |

- Set the **default (off) variation** to `control`.

### 2. Create the three funnel metrics

Under **Data → Metrics**, click **Create metric** for each step. For all three, use event kind **Custom**, keep the data warehouse on **LaunchDarkly hosted**, and in the metric definition choose **Count distinct units (Percent)** of **user** units that sent the event, where **higher is better**.

| Metric name | Event key (Custom) |
| --- | --- |
| Sign-up button clicked | `signup-button-clicked` |
| Sign-up info entered | `signup-info-entered` |
| Sign-up completed | `signup-completed` |

> The event keys must match the constants in [`src/SignupFunnel.tsx`](./src/SignupFunnel.tsx) exactly.

### 3. Create the funnel metric group

Under **Data → Metrics → Metric groups**, click **Create metric group**:

- **Type:** Funnel
- **Name:** `New plan sign-up flow`
- Add the metrics as steps **in this order** (order matters — it must match the user's journey):
1. Sign-up button clicked
2. Sign-up info entered
3. Sign-up completed

### 4. Build the experiment

- **Create → Experiment.**
- Enter a **Name** and a **Hypothesis** (e.g. "A clearer plan-selection UI increases completed sign-ups").
- **Randomize by:** user.
- **Metric source:** LaunchDarkly. **Metrics:** select the `New plan sign-up flow` metric group.
- **Flag:** `signup-flow-variation`, on the **Default rule**.
- **Variation served to users outside the experiment:** Control.
- Choose a **Sample size** and a **Statistical approach** (Bayesian or frequentist), then **Save**.

const flagValue = useBoolVariation('my-boolean-flag', false);
### 5. Start an iteration

2. On the command line, run `yarn && yarn ios` for iOS builds and `yarn && yarn android` for Android builds.
Open the experiment's **Design** tab and click **Start**. Results appear on the **Results** tab once enough contexts have gone through the funnel.

Note for Android builds, there's an issue with Flipper interfering with streaming connections if you are using version 0.73 or eariler so please run the release build. This issue does not affect iOS builds.
> **Seeing results requires many distinct contexts.** This sample identifies a single static user (`{ kind: 'user', key: 'test-hello' }` in `App.tsx`). An experiment needs many unique contexts to accumulate exposures and conversions — in a real app, use a unique `key` per user. To generate sample traffic, change the context key and re-run the funnel, or drive the flow from multiple devices/users.

You should receive the message ”Welcome to LaunchDarkly Flag value is <true/false>”.
### 6. Review observability data

To start fresh, run `yarn clean-reset`. To reset watchman, run `yarn watchman-reset`.
After the app runs on a development build, it automatically sends observability data (errors, logs, traces, and product-analytics events) to LaunchDarkly. View it under **Observability**. Because each funnel step calls `ldClient.track()`, those events also appear as `track` product-analytics events you can chart as time series and funnels. To learn more, read [Observability](https://launchdarkly.com/docs/home/observability) and [Product analytics events](https://launchdarkly.com/docs/home/observability/product-analytics).
17 changes: 11 additions & 6 deletions examples/React-Native/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,6 @@
"orientation": "portrait",
"icon": "./assets/icon.png",
"userInterfaceStyle": "light",
"splash": {
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
},
"assetBundlePatterns": [
"**/*"
],
Expand All @@ -27,6 +22,16 @@
"web": {
"favicon": "./assets/favicon.png"
},
"newArchEnabled": true
"plugins": [
[
"expo-splash-screen",
{
"image": "./assets/splash.png",
"resizeMode": "contain",
"backgroundColor": "#ffffff"
}
],
"expo-status-bar"
]
}
}
Loading
Loading