Integrate a React Native app
The complete React Native / Expo path, top to bottom. Covering every platform in one document instead — that's the full guide.
1. Get a project key
Every install needs a key shaped ah_xxxxxxxx. Create one at /projects/new, or run ah projects list if the CLI is already authenticated. Do not invent a key and do not proceed without one — the SDKs disable themselves without a key.
One project covers every platform a single product ships on — website, mobile app, and game share the same key and land in the same dashboard. Use a separate project only for a separate product, so their stats stay clean.
2. React Native / Expo install
npm i @brightmotion/agenthog-react-native npx expo install @react-native-async-storage/async-storage # for persistent ids
Always pass storage. It is not auto-detected, and omitting it is silent data loss rather than a crash: ids and the queued event buffer stay in memory, so every app restart looks like a new visitor and returning-user metrics are wrong. Use asyncStorage from the subpath, or any { getItem, setItem, removeItem } adapter returning promises (MMKV, SQLite). The subpath import is what pulls the optional peer into the bundle — importing it without installing AsyncStorage fails the Metro build.
Wrap the app at its root — app/_layout.tsx in Expo Router, inside any gesture-handler root and outside the navigator:
import { AgentHogProvider } from '@brightmotion/agenthog-react-native'
import { asyncStorage } from '@brightmotion/agenthog-react-native/async-storage'
<AgentHogProvider config={{
host: 'http://agenthog.io',
projectKey: process.env.EXPO_PUBLIC_AGENTHOG_KEY ?? '',
enabled: !!process.env.EXPO_PUBLIC_AGENTHOG_KEY, // inert no-op when the key is absent
appName: 'myapp',
appVersion: '1.0.0',
storage: asyncStorage, // omit → in-memory, ids reset every launch
}}>
{children}
</AgentHogProvider>Requires SDK ≥0.2.1. On ≤0.2.0 the /async-storage subpath does not exist and the SDK tried to locate AsyncStorage itself — a lazy require Metro cannot resolve statically, producing a fatal Requiring unknown module redbox that no try/catch suppresses. Pinned to ≤0.2.0? Pass the peer straight through: import AsyncStorage from '@react-native-async-storage/async-storage' then storage: AsyncStorage.
Put the key in .env as EXPO_PUBLIC_AGENTHOG_KEY=ah_xxxxxxxx. The enabled gate above is the idiom for keeping analytics off in local dev and in forks that have no key.
Screen views are not automatic — the SDK cannot know your router. Feed it a pathname:
import { usePathname } from 'expo-router'
import { useScreenTracking } from '@brightmotion/agenthog-react-native'
useScreenTracking(usePathname()) // emits `pageview: <path>` + a `leave` for the previous screenReact Navigation has no usePathname; derive it from the navigation state (useNavigationContainerRef().getCurrentRoute()?.name) and pass that instead.
Then, anywhere inside the provider:
const ah = useAgentHog()
ah.capture('photo_sent', { recipients: 3 })
ah.identify(email, { clerk_id: user.id })
ah.reset() // sign-out: new anon id + sessionTap autocapture is on by default (a fiber walk produces click: <label>); pass autocapture: false to disable it. For scroll-depth events, spread the hook onto a screen’s primary scroller — one per screen, not every list:
<FlatList {...useScrollDepth()} … />3. Event naming is a contract
The CLI, funnels, and dashboard parse these exact shapes. Autocapture already emits them; match the style when you add your own.
pageview: /pricing click: Join the waitlist form_submit: waitlist input: email scroll: 75% leave: /pricing
- · Custom names are stored verbatim. Short, readable, stable —
checkout_completed, notevt_CHECKOUT_v2or a UUID. - · Keep varying values out of the name.
capture('plan_selected', { plan: 'pro' }), nevercapture('plan_selected_pro')— props are queryable with--by props.plan, names baked with values are not. - ·
identify(email)is what stitches a person across devices. Call it at sign-in and after sign-up. A bare id works but will not merge across devices. - · Never put secrets, tokens, or full PII blobs in props — anyone with dashboard access can read them.
4. Verify before reporting success
Compiling is not evidence. Load the site or run the app, click through a few screens, then confirm data arrived:
ah events --since 24h # fastest proof, if the CLI is authenticated ah digest # sessions, sources, and top events in one report
Otherwise check the dashboard. Expect a pageview: row within seconds — the web tracker flushes every 5s or 10 queued events; React Native, Unity, and Capacitor every 10s or 20 events. Unity editor Play mode sends real events too (registered prop platform: editor).
5. Feature flags & A/B tests
Create a flag with ah flags create checkout_cta --variants control:50,b:50, then read it — the SDK assigns the variant deterministically per user and records exposure automatically (one $exposure event, plus $ff/checkout_cta on every later event, so ah funnel signup --by flag:checkout_cta just works):
const ah = useAgentHog()
await ah.flagsReady()
const v = ah.flag('checkout_cta') // 'control' | 'b' | undefined
// undefined = ruleset not loaded / not enrolled / killed → code defaultFull loop — experiments, results, verdict gates, rollout percentages, kill switch: Experiments.
6. When events are missing
Work through this before adding more instrumentation.
| Symptom | Cause |
|---|---|
| Nothing at all | enabled resolved false — EXPO_PUBLIC_AGENTHOG_KEY was unset at bundle time. Restart the bundler after editing .env |
Taps but no pageview: | useScreenTracking was never wired to the router |
| Every session is a new visitor | No storage adapter passed — it is not auto-detected |
Redbox: Requiring unknown module | SDK ≤0.2.0 self-detecting the peer — upgrade to ≥0.2.1 and pass storage explicitly |
| Traffic looks inflated | You are looking at bots — aggregates exclude them by default, --all includes them |
7. After integrating
Two follow-ups worth offering:
- · Define a conversion goal so sessions get marked converted:
ah goals set signup "form_submit: waitlist" - · Give the agent the read surface — install the ah CLI and add an AgentHog section to
CLAUDE.mdso future sessions query analytics instead of guessing.