@brightmotion/agenthog-react-native
v0.8.0
Published
AgentHog analytics for React Native / Expo — sessions, screen views, tap autocapture, scroll depth, identity. Speaks CONTRACTS.md directly.
Readme
@brightmotion/agenthog-react-native
AgentHog analytics for React Native / Expo. Speaks CONTRACTS.md (POST /ingest) directly —
sessions, screen views (pageview: <path>), tap autocapture (click: <label> via fiber walk),
per-screen time (leave with duration_s/max_scroll), scroll depth, custom events, identity.
Spec: docs/RN_SDK_SPEC.md. Pure JS — no native code, no runtime deps.
import { AgentHogProvider, useAgentHog, useScreenTracking, useScrollDepth } from '@brightmotion/agenthog-react-native'
import { asyncStorage } from '@brightmotion/agenthog-react-native/async-storage'
import { expoInstallReferrer } from '@brightmotion/agenthog-react-native/install-referrer'
// root layout — wrap the app (inside your gesture-handler root)
<AgentHogProvider config={{
host: 'https://hog.brightmotion.io',
projectKey: process.env.EXPO_PUBLIC_AGENTHOG_KEY ?? '',
enabled: !!process.env.EXPO_PUBLIC_AGENTHOG_KEY,
appName: 'myapp', appVersion: '1.0.0',
storage: asyncStorage, // omit → in-memory, ids reset every launch
installReferrer: expoInstallReferrer, // Expo: automatic install attribution (see below)
}}>
{children}
</AgentHogProvider>
// anywhere inside — screens + identity
useScreenTracking(usePathname()) // expo-router; feed any router's pathname
const ah = useAgentHog()
ah.capture('photo_sent', { recipients: 3 })
ah.identify(email, { clerk_id }) // email = cross-device stitch key
// Manual campaign attribution, as if the session arrived on a `?utm_...` web
// URL. With `installReferrer` configured this happens automatically on first
// launch; keep this for other sources (web handoffs, invite codes). Call as
// early as possible; params set after the session's first flush don't take effect.
ah.setLandingParams({ utm_source: 'web', utm_campaign: 'invite' })
// MMP attach (e.g. Singular's attribution callback) → the session's utm_* columns.
// Callable any time; delivered once per distinct payload; deep-link params still win.
ah.setAttribution({ provider: 'singular', utm_source: network, utm_campaign, params: { campaign_id } })
ah.reset() // sign-out
// a scrollable screen's primary scroller (optional, for scroll-depth events)
<FlatList {...useScrollDepth()} ... />Install attribution (0.4.0)
Pass an installReferrer provider and the SDK does install attribution by itself, the way
MMP SDKs do: once per install, on first launch, gated ahead of the session's first flush
(≤1.5 s, local IPC), it reads the Play Install Referrer, feeds its plaintext utm_* into the
landing-params pipe (session utm columns → Top sources / campaigns), and ships the raw
referrer string as context.install — the server classifies it and decrypts Meta's encrypted
campaign payload with the key from your AgentHog project settings (Settings → Install
attribution). No decryption key ever ships in the app.
// Expo (expo-application is already in the Expo SDK):
import { expoInstallReferrer } from '@brightmotion/agenthog-react-native/install-referrer'
installReferrer: expoInstallReferrer
// Bare RN — bring your own reader (e.g. the play-install-referrer package):
installReferrer: async () => (await PlayInstallReferrer.getInstallReferrerInfo()).installReferreriOS resolves null (the App Store has no referrer — that's Apple's design, not a gap);
sideloads and Play-less devices read as organic/none. Needs storage configured — the
once-per-install flag lives there.
The server answers the install batch with its classification, which the SDK caches for the app's lifetime and exposes (0.5.0) — use it to put campaign props on events or person traits:
ah.onAttribution(({ source, utm, meta }) => { // fires once; replayed from cache on later launches
ah.register({ attribution_source: source, ...utm })
if (meta) ah.identify(undefined, { meta_campaign: meta.campaign_group_name })
})
const cached = await ah.getAttribution() // null until the install batch round-tripsStorage
Persistence is opt-in: pass storage, or anon ids and the queued event buffer live
in memory only and every app restart looks like a brand-new visitor. Two options:
import { asyncStorage } from '@brightmotion/agenthog-react-native/async-storage' // needs the peer
storage: asyncStoragestorage: myAdapter // any { getItem, setItem, removeItem } returning promises (MMKV, SQLite, …)Install the peer for the first form: npx expo install @react-native-async-storage/async-storage.
The SDK can't find AsyncStorage for you, and the separate entry point is why. Metro
collects dependencies statically from literal import/require forms, so a lazy
lookup never enters the bundle graph — and when it fails, Metro's guardedLoadModule
routes the error to ErrorUtils.reportFatalError (a dev redbox that no surrounding
try/catch can suppress) before returning undefined. Versions ≤0.2.0 tried exactly
that and got both halves wrong: a crash and a silent fall back to memory. A literal
top-level import in the main entry would resolve correctly but make the optional peer
mandatory for everyone, breaking the bundle for consumers who bring their own adapter.
Importing /async-storage is what puts the peer in the graph, and only for the apps that
actually want it.
Dev: bun test packages/react-native. Ship to a consumer repo:
npm pack → commit the tarball to <app>/vendor/ → npm i file:vendor/agenthog-react-native-<v>.tgz.
