Repository navigation
React Native example: Expo 57 / TS 7 / Babel 8 upgrade + Observability + funnel experiment + CI #18
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
Open
professorice
wants to merge
12
commits into
main
Choose a base branch
from
mzafir/react-native-example-upgrade
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+2,966
−4,088
Open
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
7319e5d
chore(react-native example): upgrade to Expo 57, TS 7, Babel 8, add O…
professorice 72a53d8
feat(react-native example): show all Observability options + funnel e…
professorice e418b4e
fix(react-native example): don't enable flag-exposure dedupe in the s…
professorice b3994ac
refactor(react-native example): trim Observability options to the ess…
professorice 223c970
fix(react-native example): address Copilot review feedback
professorice ee8b796
Merge remote-tracking branch 'origin/main' into mzafir/react-native-e…
professorice 0b2d7b8
fix(react-native example): address follow-up review comments
professorice 1e510ae
ci: pin CI to Node 22 LTS; bump README Node floor to 22
professorice c0e06b7
fix(react-native example): address review comments
professorice c7cd033
fix(react-native example): address Copilot review round
professorice 55e85af
ci: add explicit yarn install after yarn up in the latest-canary job
professorice 9d826af
ci: use --immutable for the canary post-yarn-up install
professorice File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| - name: Bundle ${{ matrix.platform }} | ||
| run: yarn expo export --platform ${{ matrix.platform }} --output-dir dist-${{ matrix.platform }} | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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', | ||
| }, | ||
|
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> | ||
| ); | ||
|
professorice marked this conversation as resolved.
|
||
| } | ||
|
|
||
| const styles = StyleSheet.create({ | ||
| loading: { | ||
| flex: 1, | ||
| alignItems: 'center', | ||
| justifyContent: 'center', | ||
| backgroundColor: '#fff', | ||
| }, | ||
| loadingText: { marginTop: 12, color: '#555' }, | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.