The agent skill

A skill is a markdown file an agent loads when it becomes relevant. This one teaches Claude Code how to integrate AgentHog properly: which SDK a repo needs, where the snippet actually goes, how to relay backend events that no SDK can see, the event-naming rules the CLI depends on, and how to verify events arrived instead of declaring victory at compile time.

View rawPaste it into any agent that reads a single instructions file.
  1. 1

    Install it

    On Claude Code, install the plugin. It carries both skills — integration and experiments — and later updates arrive with /plugin marketplace update agenthog instead of another curl:

    /plugin marketplace add AnniesAI/agenthog-claude
    /plugin install agenthog@agenthog

    Or take the raw markdown. This is the path for any other agent, and the one to use against a self-hosted AgentHog — the file served here has its host rewritten to this deployment, while the plugin’s copy points at https://agenthog.io. Personal install, available in every project on this machine:

    mkdir -p ~/.claude/skills/agenthog-integrate && \
      curl -fsSL http://agenthog.io/skill/SKILL.md \
        -o ~/.claude/skills/agenthog-integrate/SKILL.md

    Or scope it to one repo and commit it, so everyone on the team (and every cloud session) gets it:

    mkdir -p .claude/skills/agenthog-integrate && \
      curl -fsSL http://agenthog.io/skill/SKILL.md \
        -o .claude/skills/agenthog-integrate/SKILL.md
  2. 2

    Use it

    Claude Code picks up new skill files during the session. If ~/.claude/skills/ did not exist before you ran the command, restart Claude Code once so the directory gets watched.

    Then either invoke it directly or just describe what you want — the description tells Claude when to reach for it on its own:

    /agenthog-integrate          # installed from markdown
    /agenthog:agenthog-integrate   # installed as the plugin
    
    # or simply, either way:
    "add AgentHog analytics to this app"
  3. 3

    Have a project key ready

    The skill will ask for one rather than guess. Create a project to get a key shaped ah_xxxxxxxx, then paste it when the agent asks.

    Create a project →

The experiment skill

A second, separate skill covers running A/B tests once AgentHog is integrated: create a flag, wire the variant read, verify exposure, open an experiment window with an honest horizon, read results without declaring early winners, ship and retire. Different job, different trigger phrases (“set up an A/B test”, “check the experiment results”, “roll out to 10%”) — see Experiments for the feature itself.

The plugin above already includes it — nothing further to install. To take it as markdown instead:

mkdir -p ~/.claude/skills/agenthog-experiment && \
  curl -fsSL http://agenthog.io/skill/experiment/SKILL.md \
    -o ~/.claude/skills/agenthog-experiment/SKILL.md

Other agents and editors

Skills follow the Agent Skills open standard, so the same file works beyond Claude Code. For tools that read a single instructions file instead of a skills directory, point them at the raw URL or paste the contents in.

ToolWhere to put it
Claude CodeThe plugin (agenthog@agenthog), or ~/.claude/skills/agenthog-integrate/SKILL.md (personal) or .claude/skills/ (project)
Agent Skills toolsSame layout — a directory containing SKILL.md
Anything elseFetch http://agenthog.io/skill/SKILL.md and append it to the project’s rules or instructions file

What the skill says

No magic — it is the page next door, written for a machine. Read it before you install it; that is good hygiene for any skill, since a skill is instructions your agent will follow.

View raw
---
name: agenthog-integrate
description: Install AgentHog analytics into a web app, a React Native / Expo app, a Capacitor hybrid app, or a Unity game, send server-side events from a backend, name events the way the AgentHog contract requires, and verify data is arriving. Use when adding analytics to a project, instrumenting events or conversions, relaying webhooks or backfilling history into analytics, or when AgentHog, ah.js, @brightmotion/agenthog-react-native, @brightmotion/agenthog-capacitor, or com.brightmotion.agenthog is mentioned.
when_to_use: The user asks to "add analytics", "set up AgentHog", "track signups", "instrument this app/game", "track subscriptions/revenue", "send events from our backend", "why is AgentHog not receiving events", or names any AgentHog package (including a Capacitor/Ionic app).
---

# 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 `http://agenthog.io/projects/new` and paste the key, or
- If the `ah` CLI is installed and authenticated, run `ah projects list` and 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.txt` or a `Packages/manifest.json` with `com.unity.*`
  dependencies → it is a **Unity game** (§5)
- `package.json` with `react-native` or `expo` → **React Native install** (§4)
- `capacitor.config.ts` / `capacitor.config.json` (or `@capacitor/core` in 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.
  A `config.xml` with 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:

```html
<script src="http://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`:

```js
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
```

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

```bash
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:

```tsx
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>
```

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:

```tsx
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:

```tsx
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:

```tsx
<FlatList {...useScrollDepth()} … />
```

## 5. Unity install

Add the UPM package to `Packages/manifest.json` (or Package Manager → *Add package from
git URL*):

```json
"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 in `Assets/Resources/AgentHogSettingsLocal.asset`
  (gitignored) — the `Local` variant takes precedence.
- **Code:** `AgentHog.Init(new AgentHogConfig { Host = "http://agenthog.io", ProjectKey = "ah_xxxxxxxx" })`
  once at startup. With a blank key or `Enabled = false` every 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`:

```csharp
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
`http://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)

```bash
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:

```ts
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',                              // 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.

```ts
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 `http://agenthog.io/tokens`.

```bash
curl -s http://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; the `ah_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 fresh `sessionId`
  per delivery, and `identify` carrying the same email or `user_id` the app's
  `identify()` 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 traffic` by design (a webhook is not a visit) and
  visible by default in `ah 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`):

```bash
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:
`http://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`, not `evt_CHECKOUT_v2` or a UUID.
- Use `snake_case` and keep the name free of interpolated values. Put the varying part in
  props: `capture('plan_selected', { plan: 'pro' })`, never `capture('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:

1. Load the site (or run the app) and click through two or three screens.
2. Check, in order of preference:
   - `ah events --since 24h` — if the `ah` CLI is authenticated, this is the fastest proof
   - `ah digest` — sessions, sources, and top events in one report
   - the dashboard at `http://agenthog.io` otherwise
3. 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 prop `platform: 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 --apply` writes them; `ah revenue config` confirms they hit real
  events; `ah revenue` and `ah ltv` then 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 `ah` CLI (`npm i -g @brightmotion/agenthog`,
  then `ah login`) and add a short AgentHog section to `CLAUDE.md` so future sessions query
  analytics instead of guessing. Full CLI docs: `http://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:
`curl -fsSL http://agenthog.io/skill/experiment/SKILL.md -o ~/.claude/skills/agenthog-experiment/SKILL.md`