Integrate AgentHog
AgentHog is web + mobile analytics with an agent-readable CLI. Integrating it means: picking the right SDK for the project, wiring it in once, and confirming events land.
Most of the value is in autocapture — you do not hand-instrument pageviews, clicks, forms, or scroll depth. Add the SDK, then add custom events only for things autocapture cannot see (server-side outcomes, purchases, feature usage).
1. Get a project key
Every install needs a project key shaped ah_xxxxxxxx. If the user has not given you one:
- Ask them to create a project at
https://agenthog.io/projects/newand paste the key, or - If the
ahCLI is installed and authenticated, runah projects listand use an existing key.
Project creation is capped by plan (Free: 1 project). If /projects/new shows an upgrade
notice instead of the create form, the account is at its cap — stop and let the user
either reuse an existing project's key or upgrade; there is nothing to integrate
without a key.
Do not invent a key and do not proceed without one — the tracker silently disables itself
when data-project is missing.
2. Pick the platform
Inspect the repo before choosing:
ProjectSettings/ProjectVersion.txtor aPackages/manifest.jsonwithcom.unity.*dependencies → it is a Unity game (§5)package.jsonwithreact-nativeorexpo→ React Native install (§4)capacitor.config.ts/capacitor.config.json(or@capacitor/corein package.json) → a Capacitor hybrid app (§6). Check this BEFORE the web rule — a Capacitor repo serves HTML too, but the web tag inside its WebView misclassifies every session. Aconfig.xmlwith Cordova and NO Capacitor config is a legacy Cordova app the SDK does not target; if it is mid-migration to Capacitor, instrument the Capacitor version.- anything else that serves HTML (Next.js, Astro, Remix/React Router, Rails, plain HTML) → web install (§3)
A repo can need both (marketing site + mobile app/game). They are separate projects in AgentHog — use a different project key for each, so their stats stay clean.
Events that no client can witness — a billing webhook, a queue worker, a backfill of existing history — are not an SDK install at all; they go straight to the ingest endpoint from the backend (§7).
3. Web install
One script tag, before </head>, on every page:
<script src="https://agenthog.io/ah.js" data-project="ah_xxxxxxxx" defer></script>
Put it wherever that framework renders <head> site-wide — app/layout.tsx (Next.js App
Router), pages/_document.tsx (Pages Router), app/root.tsx (React Router / Remix),
src/layouts/*.astro (Astro), the shared layout template otherwise. Do not add it per-page.
Optional attributes:
| attribute | when you need it |
|---|---|
data-api="https://your-agenthog.example" |
self-hosted backend on a different origin than the script |
data-cookie-domain=".example.com" |
share one visitor id across subdomains (www. and app.) |
What you get with no further work: pageviews (including SPA route changes — pushState,
replaceState, and popstate are all 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.
The global API for custom work is window.agenthog:
window.agenthog.capture('checkout_completed', { plan: 'pro', cents: 4900 })
window.agenthog.identify('user@example.com', { 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
Guard these behind a typeof window !== 'undefined' && window.agenthog check in SSR
frameworks — the object only exists in the browser, 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,
not a crash: ids and the queued event buffer stay in memory, so every app restart looks
like a brand-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 — in Expo Router that is app/_layout.tsx, 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: 'https://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>
The npm i above installs the current SDK, which is what the /async-storage subpath
needs. Much older releases (before 0.2.1) had no such subpath and tried to locate
AsyncStorage themselves — a lazy require Metro cannot resolve statically, producing a fatal
redbox (Requiring unknown module) that no try/catch suppresses. Upgrade rather than work
around it; if a project is stuck on one of those, pass the peer straight through:
import AsyncStorage from '@react-native-async-storage/async-storage' → 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 from whatever router the app uses:
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.
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 (fiber walk → click: <label>); pass autocapture: false
to disable. For scroll-depth events on a scrollable screen, spread the hook onto the 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):
"com.brightmotion.agenthog": "https://github.com/AnniesAI/agenthog-unity.git?path=com.brightmotion.agenthog"
With no #tag, UPM resolves the latest default-branch commit when the package is added and
records it in Packages/packages-lock.json, so the version stays fixed until someone updates
it deliberately. To pin to a release instead, append the newest tag from
https://github.com/AnniesAI/agenthog-unity/releases (e.g. #v0.3.0).
Unity 2021.3+; pure C#, zero dependencies, no native plugins. Two ways to configure — pick the settings asset unless the game already has a bootstrap script:
- Settings asset (no code): Assets → Create → AgentHog → Settings, save as
Assets/Resources/AgentHogSettings.asset, fill in host + project key. The SDK initializes itself on startup. In a public/shared repo, commit that asset blank (the SDK stays inert) and put the real key inAssets/Resources/AgentHogSettingsLocal.asset(gitignored) — theLocalvariant takes precedence. - Code:
AgentHog.Init(new AgentHogConfig { Host = "https://agenthog.io", ProjectKey = "ah_xxxxxxxx" })once at startup. With a blank key orEnabled = falseevery call is a safe no-op, so call sites never need guards.
What is automatic: sessions (30-min idle, survives restarts), scene loads as
pageview: /scene-name, uGUI taps as click: <label>, per-screen time via leave
events, device context on every event, offline/crash carry-over. What is NOT automatic:
single-scene games with UI panels should call AgentHog.Screen("/shop") on 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 }); // games rarely have emails — a stable user_id trait still stitches identity
AgentHog.Reset(); // sign-out: device becomes a new anonymous person
AgentHog.SetLandingParams(new Dictionary<string, string> { ["utm_source"] = "playstore" }); // deep-link params — call before the first flush
Using Singular for install attribution? Point its Internal-BI postbacks at AgentHog:
generate the postback URL in project settings ("Install attribution — Singular postbacks")
and paste it into Singular as the app's Internal BI postback endpoint. Singular POSTs its
standard JSON payload in full — in the game, call SingularSDK.SetCustomUserId(...) with an
id AgentHog knows (the player's own user_id also sent via Identify, or AgentHog.AnonId)
so the payload's user_id identifies the player, and enable the install (and re-engagement)
postbacks. Point Singular at the URL directly, or relay a copy through the game's own
backend — both hookups, and when to prefer which, are covered at
https://agenthog.io/docs/server#postbacks. Network,
campaign and creative names then land on the install session's utm_* columns (deep-link
params keep precedence, and an "organic" answer never erases the built-in referrer verdict)
and surface in ah campaigns, ah user <ref> (first/latest touch), and
ah users --source <network>.
Full docs: https://github.com/AnniesAI/agenthog-unity.
6. Capacitor install (hybrid apps)
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:
import { AgentHog } from '@brightmotion/agenthog-capacitor'
await AgentHog.init({
host: 'https://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', // ideally from App.getInfo()
})
Calls made before init resolves are buffered and replayed — nothing drops. Unlike RN
there is no storage to configure: ids and the offline queue persist through
@capacitor/preferences (native storage, survives WebView data eviction).
What is automatic: screen views from the router (history AND hash routing are 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 with field names only, scroll depth), 30-min-idle
sessions across app background/foreground, and a persisted offline queue with
crash/kill 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 (AgentHogCap/… — the session classifies
as mobile app traffic) and no Origin header (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 it means a browser preview needs the
preview host in the project's domains.
7. Server-side events (no SDK)
Some outcomes have no client running when they happen: a subscription renews, a provider
webhook lands, a nightly job decides someone churned, or the user wants existing history
backfilled. Those go to the same /ingest endpoint from the backend, authenticated with a
write-scope token — an org admin mints it at https://agenthog.io/tokens.
curl -s https://agenthog.io/ingest \
-H "Authorization: Bearer $AGENTHOG_INGEST_TOKEN" \
-H 'content-type: application/json' \
-d '{"project":"ah_xxxxxxxx","anonId":"server:42","sessionId":"<uuid>",
"identify":{"traits":{"user_id":"42"}},
"events":[{"ts":1754870400000,"type":"custom","name":"subscription_started",
"props":{"tier":"plus","price_usd":9.99}}]}'
What matters when you write the relay:
- The token is a secret — backend environment only, never a client bundle, an app
binary, or a committed file. The
ah_project key is public; theah_tok_token is not. - With the token, bot scoring is skipped (the session classifies as
server) and event timestamps are trusted, which is what lets a backfill land on the right days. Omitting the header still returns 204 — the batch just takes the anonymous path, gets bot-scored from your server's datacenter IP, and has its timestamps clamped to ±10 minutes of now. - Conventions that make identity work:
anonId: "server:<user_id>", a freshsessionIdper delivery, andidentifycarrying the same email oruser_idthe app'sidentify()sends. That is what merges the server timeline with the device one. - There is no idempotency key — a webhook provider that retries produces a duplicate event. Put the provider's own event id in props if dedupe matters.
- Make it fire-and-forget. Analytics must never fail the request that triggered it.
- Server sessions are excluded from
ah trafficby design (a webhook is not a visit) and visible by default inah events.
For a one-off — logging that a deploy or migration happened, or a one-shot import of
history from a spreadsheet or another tool — do not write a relay. The ah CLI wraps this
call with every convention above applied (ah track --help):
ah track deploy_completed --prop service=app --prop version=1.9.2
ah track --file history.csv --event signed_up --ts-col Date --email-col Email --dry-run
--dry-run first: it prints the row count, date range, bad rows by line, and a sample
batch, and the real run refuses bad rows unless --skip-bad. A bare --at date is UTC
midnight; a wall-clock time with no offset is the machine's local zone — the verb echoes
which. No idempotency key: a second run writes every row again.
Full reference, including the subscription-lifecycle event names the mobile dashboard reads:
https://agenthog.io/docs/server.
8. Event naming — this is a contract, not a style preference
The CLI, funnels, and dashboard parse these exact shapes. Autocapture already emits:
pageview: /pricing click: Join the waitlist form_submit: waitlist
input: email scroll: 75% leave: /pricing
Rules that matter when you add custom events:
- Custom event names are stored verbatim. Pick short, human-readable, stable names —
checkout_completed, notevt_CHECKOUT_v2or a UUID. - Use
snake_caseand keep the name free of interpolated values. Put the varying part in props:capture('plan_selected', { plan: 'pro' }), nevercapture('plan_selected_pro'). Props are queryable via--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. Prefer email; a bare id still works but will not merge across devices.- Send the email in whatever case you have it — AgentHog trims and lowercases addresses on arrival, so a phone that auto-capitalized the form field still resolves to one person. Do not lowercase a non-email id before sending: those are stored verbatim and matched exactly, so folding the case yourself can merge two different users.
- Never put secrets, tokens, or full PII blobs in props — props are readable by anyone with dashboard access.
9. Verify before you report success
Do not tell the user it works because the code compiles. Confirm data arrived:
- Load the site (or run the app) and click through two or three screens.
- Check, in order of preference:
ah events --since 24h— if theahCLI is authenticated, this is the fastest proofah digest— sessions, sources, and top events in one report- the dashboard at
https://agenthog.iootherwise
- Expect a
pageview:row within a few seconds — the web tracker flushes every 5s or every 10 queued events; React Native, Unity, and Capacitor flush every 10s or every 20 events. Unity editor Play mode sends real events too (registered propplatform: editor).
If nothing arrives, work through §10 rather than adding more instrumentation.
10. When events are missing
| symptom | cause |
|---|---|
console: [agenthog] missing data-project — tracker disabled |
no data-project attribute, or it is an empty template value |
no requests to /ingest at all |
script blocked by an ad blocker, a CSP script-src, or never rendered — check the built HTML, not the source |
| web events stop after the first page | the tag was added to one page instead of the shared layout |
| RN: nothing at all | enabled resolved false because EXPO_PUBLIC_AGENTHOG_KEY was unset at bundle time — restart the bundler after editing .env |
RN: no pageview: rows, but taps appear |
useScreenTracking was never wired to the router |
| every session is a new visitor | no storage adapter passed (RN — it is not auto-detected), or cookies blocked (web) |
RN redbox: Requiring unknown module "@react-native-async-storage/async-storage" |
an old SDK (before 0.2.1) self-detecting the peer; upgrade to the current SDK and pass storage explicitly |
| Unity: nothing at all | the settings asset has a blank key (committed-blank is the intended public-repo default — the real key belongs in AgentHogSettingsLocal.asset), or Init never ran. Set debugLog = true and watch the Console for [AgentHog] lines |
Unity: clicks show GameObject names like click: BtnStart2 |
the pressed control has no Text/TMP child — autocapture falls back to the GameObject name. Add label text or rename the object |
| Unity: gameplay taps missing | autocapture covers uGUI (Canvas) only — world-space objects and UI Toolkit need explicit Capture calls |
| Capacitor: sessions classify as Chrome/Safari and miss the mobile dashboard | that was a web/PWA run of the bundle (browser preview) — only native builds take the CapacitorHttp path that sets the app UA. If it IS a device build, check @capacitor/core ≥5 |
Capacitor: 403 from /ingest |
the same 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 |
server relay: 401 / 403 from /ingest |
token invalid or revoked (401), or read-scope / wrong project (403). A bad token is a hard fail by design — it never silently falls back |
| server relay: 204s, but events are bot-scored or all dated today | the Authorization header is missing, so the batch took the anonymous path (§7) |
| traffic looks inflated | you are seeing bots; aggregates exclude them by default, --all includes them |
| your own testing shows up as users | run ah test-ips add from that network (no ip needed — the server uses the address you call from). Its traffic becomes classification: test — kept, but out of the default filter, and applied retroactively to what it already did. ah test-ips rm <ip> reverses it |
| test traffic still appears after listing the IP | the device is probably on cellular, where the carrier IP churns — a list only matches stable (wifi) IPs |
11. After integrating
Two follow-ups worth offering the user:
- Define a conversion goal so sessions get marked converted:
ah goals set signup "form_submit: waitlist" --project <name> - Map revenue if any event carries money. This is config, not another integration: you
declare which event holds the amount and in which prop, and it applies retroactively over
all history. Let it find the mapping rather than typing one:
ah revenue suggest→ prints the exact commands (and the share of occurrences whose amount actually parses, which is how you catch a wrong prop before it lands in a total).ah revenue suggest --applywrites them;ah revenue configconfirms they hit real events;ah revenueandah ltvthen work everywhere, including--by utm_source. Purchases relayed by a webhook (§7) count without any extra flag. - Give their agent the read surface: install the
ahCLI (npm i -g @brightmotion/agenthog, thenah login) and add a short AgentHog section toCLAUDE.mdso future sessions query analytics instead of guessing. Full CLI docs:https://agenthog.io/docs/cli.
To run an A/B test on top of this install (feature flags, experiments, variant metrics), use the separate agenthog-experiment skill, which ships alongside this one. To write up the numbers as a shared report the whole organization reads in the dashboard (Markdown with live charts), use the agenthog-report skill, which also ships alongside this one.