How to integrate
Written for an agent doing the work. Read it top to bottom: the decision in step 2 determines everything after it. Prefer a single linear path? Each platform has its own page: Web, React Native, Capacitor, or Unity.
Handing this to Claude Code?
Install the skill instead of pasting this page — it loads itself when relevant and stays out of context until then.
Install the agent skill →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. Decide the platform
Inspect the repo before choosing:
| If you find | Do this |
|---|---|
ProjectSettings/ProjectVersion.txt | or Packages/manifest.json with com.unity.* deps → it's a Unity game → the Unity install |
react-native | or expo in package.json → the React Native install |
capacitor.config.ts | or .json, or @capacitor/core in package.json → a Capacitor hybrid app → the Capacitor install. Check this before the web rule — a Capacitor repo serves HTML too, but the web tag inside its WebView misclassifies every session. A Cordova config.xml with no Capacitor config is a legacy app the SDK does not target. |
| Anything serving HTML | Next.js, Astro, React Router, Rails, static HTML → the web install |
| Both | One product on several platforms is one project — install both SDKs with the same key and they land in the same dashboard. Use a second project only for a genuinely separate product. |
3. Web install
One script tag, before the closing head tag, on every page:
<script src="http://agenthog.io/ah.js" data-project="ah_xxxxxxxx" defer></script>
Add it where the framework renders <head> site-wide — app/layout.tsx for the Next.js App Router, pages/_document.tsx for the Pages Router, app/root.tsx for React Router or Remix, the shared src/layouts/*.astro for Astro. Never per-page.
Optional attributes:
| Attribute | When you need it |
|---|---|
data-api | Self-hosted backend on a different origin than the script itself |
data-cookie-domain | Share one visitor id across subdomains — set to a registrable domain like .example.com so app. and www. resolve to the same person |
With no further work you get pageviews (SPA route changes included — pushState, replaceState, and popstate are hooked), clicks, form submits, input focus, scroll depth at 25/50/75/90/100%, and a leave event carrying time-on-page and max scroll.
For everything autocapture cannot see, the browser global is:
window.agenthog.capture('checkout_completed', { plan: 'pro', cents: 4900 })
window.agenthog.identify('[email protected]', { plan: 'pro' }) // email = cross-device stitch key
window.agenthog.tag('beta_cohort', true)
window.agenthog.register({ app_version: '2.1.0' }) // merged into every later eventIn an SSR framework, guard those behind typeof window !== 'undefined' && window.agenthog — the object exists only in the browser, only after the script runs.
4. 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()} … />5. Unity install
Add the UPM package to Packages/manifest.json (or Package Manager → Add package from git URL), pinned to a tag. Unity 2021.3+; pure C#, zero dependencies, no native plugins.
"com.brightmotion.agenthog": "https://github.com/AnniesAI/agenthog-unity.git?path=com.brightmotion.agenthog#v0.1.0"
Configure via a settings asset (no code): Assets → Create → AgentHog → Settings, saved as Assets/Resources/AgentHogSettings.asset with your host + key — the SDK initializes itself on startup. In a shared repo, commit that asset blank (SDK stays inert) and keep the real key in a gitignored Assets/Resources/AgentHogSettingsLocal.asset, which takes precedence. Or in code, once at startup:
AgentHog.Init(new AgentHogConfig {
Host = "http://agenthog.io",
ProjectKey = "ah_xxxxxxxx", // blank key or Enabled=false → every call is a safe no-op
});Automatic: sessions, scene loads as pageview: /scene-name, uGUI taps as click: <label>, per-screen time, device context, offline/crash carry-over. Not automatic: single-scene games should call AgentHog.Screen("/shop") on UI-panel changes, and gameplay (world-space objects, UI Toolkit) is instrumented with Capture:
AgentHog.Capture("level_complete", new Dictionary<string, object> { ["level"] = 12 });
AgentHog.Identify(traits: new Dictionary<string, object> { ["user_id"] = playerId });
AgentHog.Reset(); // sign-out: device becomes a new anonymous personGames rarely have emails — a stable user_id trait still stitches identity. Using Singular for install attribution? Its Internal BI postbacks deliver the full verdict (network, campaign, creative, adset) server-side: generate the postback URL in project settings and call SingularSDK.SetCustomUserId(…) in the game with your own user_id (also sent via Identify) or AgentHog.AnonId — see attribution postbacks, which also covers pointing Singular at the URL directly versus relaying a copy through your own backend. Full docs: github.com/AnniesAI/agenthog-unity.
6. Capacitor install
npm i @brightmotion/agenthog-capacitor npm i @capacitor/app @capacitor/device @capacitor/preferences # required peers, all official plugins npx cap sync
Requires Capacitor ≥5. Call init once at app bootstrap, before first render — vanilla TS, no framework wrapper; Ionic React/Vue/Angular all consume it the same way. Calls made before init resolves are buffered and replayed, so nothing drops.
import { AgentHog } from '@brightmotion/agenthog-capacitor'
await AgentHog.init({
host: 'http://agenthog.io', // https — iOS ATS applies to native requests too
projectKey: import.meta.env.VITE_AGENTHOG_KEY ?? '',
enabled: !!import.meta.env.VITE_AGENTHOG_KEY, // inert no-op without a key (dev builds)
appName: 'myapp',
appVersion: '1.0.0',
})Put the key in .env as VITE_AGENTHOG_KEY=ah_xxxxxxxx (or the equivalent for your bundler). The enabled gate above keeps analytics off in dev builds and in forks that have no key.
Unlike React Native there is no storage to configure — ids and the offline queue persist through @capacitor/preferences (native storage, survives WebView data eviction). Automatic with no further work: screen views from the router (history and hash routing both hooked; trackScreens: false + AgentHog.screen('/path') to drive manually), DOM autocapture matching the web tracker (clicks with shadow-DOM-aware labels, Ionic components included, form submits, input focus, scroll depth), 30-min-idle sessions across background/foreground, and a persisted offline queue with crash carry-over.
AgentHog.capture('run_logged', { miles: 3.1 })
AgentHog.identify(email, { user_id }) // sign-in / sign-up
AgentHog.reset() // sign-out: new anonymous person
AgentHog.onAttribution((a) => { /* install attribution result */ })The transport detail that matters: on a device the SDK posts through CapacitorHttp (native), so batches carry an app User-Agent — the session classifies as mobile app traffic — and no Origin header, so the project's domain allowlist never blocks a native build. A web/PWA build of the same bundle falls back to plain fetch and is treated as ordinary web traffic: correct, but a browser preview needs the preview host in the project's domains.
Platform-by-platform detail on what each SDK collects without being asked — and what it cannot see — lives on the Events page.
7. 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.
8. 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).
9. When events are missing
Work through this before adding more instrumentation.
| Symptom | Cause |
|---|---|
Web: Console: missing data-project | No data-project attribute, or it rendered as an empty template value |
Web: No /ingest requests at all | Ad blocker, a CSP script-src, or the tag never rendered — check the built HTML, not the source |
| Web: Only the first page tracks | The tag went on one page instead of the site-wide layout |
| Web: Every session is a new visitor | Cookies and localStorage blocked (private mode, aggressive privacy settings) |
| RN: Nothing at all | enabled resolved false — EXPO_PUBLIC_AGENTHOG_KEY was unset at bundle time. Restart the bundler after editing .env |
RN: Taps but no pageview: | useScreenTracking was never wired to the router |
| RN: Every session is a new visitor | No storage adapter passed — it is not auto-detected |
RN: Redbox: Requiring unknown module | SDK ≤0.2.0 self-detecting the peer — upgrade to ≥0.2.1 and pass storage explicitly |
| Unity: Nothing at all | The settings asset has a blank key (the intended committed default — the real key belongs in AgentHogSettingsLocal.asset), or Init never ran. Set debugLog and watch the Console for [AgentHog] lines |
Unity: Clicks named like click: BtnStart2 | The control has no Text/TMP child — autocapture fell back to the GameObject name. Add label text |
| Unity: Gameplay taps missing | Autocapture covers uGUI (Canvas) only — world objects and UI Toolkit need explicit Capture |
| Capacitor: Sessions classify as Chrome/Safari | That was a web/PWA run of the bundle — only native builds take the CapacitorHttp path that sets the app User-Agent. On a real device build, check @capacitor/core ≥5 |
Capacitor: 403 from /ingest | The web fallback hitting the domain allowlist — add the preview host to the project's domains. Native builds send no Origin and are never allowlist-blocked |
Capacitor: Taps but no pageview: on navigation | The router changes neither the URL path nor a #/ hash — call AgentHog.screen('/path') where it navigates |
| Traffic looks inflated | You are looking at bots — aggregates exclude them by default, --all includes them |
10. 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.