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 findDo this
ProjectSettings/ProjectVersion.txtor Packages/manifest.json with com.unity.* deps → it's a Unity game → the Unity install
react-nativeor expo in package.json → the React Native install
capacitor.config.tsor .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 HTMLNext.js, Astro, React Router, Rails, static HTML → the web install
BothOne 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:

AttributeWhen you need it
data-apiSelf-hosted backend on a different origin than the script itself
data-cookie-domainShare 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 event

In 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 screen

React 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 + session

Tap 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 person

Games 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, not evt_CHECKOUT_v2 or a UUID.
  • · Keep varying values out of the name. capture('plan_selected', { plan: 'pro' }), never capture('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.

SymptomCause
Web: Console: missing data-projectNo data-project attribute, or it rendered as an empty template value
Web: No /ingest requests at allAd blocker, a CSP script-src, or the tag never rendered — check the built HTML, not the source
Web: Only the first page tracksThe tag went on one page instead of the site-wide layout
Web: Every session is a new visitorCookies and localStorage blocked (private mode, aggressive privacy settings)
RN: Nothing at allenabled 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 visitorNo storage adapter passed — it is not auto-detected
RN: Redbox: Requiring unknown moduleSDK ≤0.2.0 self-detecting the peer — upgrade to ≥0.2.1 and pass storage explicitly
Unity: Nothing at allThe 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: BtnStart2The control has no Text/TMP child — autocapture fell back to the GameObject name. Add label text
Unity: Gameplay taps missingAutocapture covers uGUI (Canvas) only — world objects and UI Toolkit need explicit Capture
Capacitor: Sessions classify as Chrome/SafariThat 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 /ingestThe 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 navigationThe router changes neither the URL path nor a #/ hash — call AgentHog.screen('/path') where it navigates
Traffic looks inflatedYou 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.md so future sessions query analytics instead of guessing.