Events

What lands in the database, and how to add to it. Two layers stack up on every install: the SDK autocaptures a fixed set of events plus a per-session context blob; the server derives another layer from the request itself — device, campaign, geography, bot verdict — that no SDK has to send. Everything below is what you get before writing a single line of instrumentation.

The autocapture matrix

Same event names on all three platforms, different coverage — a browser exposes a DOM to walk, a game engine does not. Automatic means it happens with no code from you; wire it up means the SDK emits it but cannot know when without a one-line hook.

EventWebReact NativeUnity
pageview: /pathAutomaticEvery load + SPA route changeWire it upuseScreenTracking(pathname)AutomaticScene loads; Screen() for in-scene UI
click: LabelAutomaticAny interactive elementAutomaticTaps on anything pressableAutomaticuGUI only — not world objects
scroll: 75%Automatic25/50/75/90/100 per pageWire it upSpread useScrollDepth() on a scrollerNot available
form_submit: nameAutomaticEvery form submitNot availableNot available
input: fieldAutomaticFocus + change, names onlyNot availableNot available
leave: /pathAutomaticOn pagehide, with time + depthAutomaticScreen change + backgroundingAutomaticScene change + pause/quit
identifyYou call itYou call it at sign-inYou call itYou call it at sign-inYou call itYou call it at sign-in
custom eventsYou call itcapture()You call itcapture()You call itCapture()

Sessions, identity, batching, retry, and offline buffering are automatic everywhere and are not listed above — see sessions and identity.

Props on autocaptured events

Every event carries ts, type, name, and path. These props ride along, and are queryable with --by props.<key>:

EventProps
pageviewtitle (when the platform has one)
clickselector (a short DOM path, RN component path, or Unity hierarchy path), text, href (web), interactive, trusted
scrolldepth — 25, 50, 75, 90, or 100
formform, fields — field names, never values
inputfield, form, action (focus or change)
leaveduration_s, plus max_scroll on web and React Native

Labels follow one rule across platforms, so click: Buy now means the same thing in a game and on a landing page: visible text first (collapsed, ≤50 chars), then the accessible label, then an identifier, then the element/GameObject name. A click on an unlabelled control shows up under its raw name — that is the signal to add label text, not to hand-instrument the button.

Web specifics

The script tag hooks pushState, replaceState, and popstate, so client-side routers produce real pageviews. leave fires once per page load — on pagehide, carrying duration_s and max_scroll — not on every SPA transition. Input events are debounced to one per field per second, and password and hidden fields are skipped entirely rather than reported by name.

The tracker also runs eight environment checks (webdriver flags, headless UAs, automation artifacts, an offscreen honeypot link, untrusted click events, and so on) and ships only the names of the ones that fired. Those feed the bot score — they are not fingerprinting, and no value from them is stored per visitor.

React Native specifics

Taps are captured by walking React’s fiber tree from the touch target to the nearest onPress, so pressables from any UI kit are covered without registration. Movement over ~8px counts as a swipe, not a tap. Screen views are the one thing the SDK cannot infer — it has no idea what your router is — so useScreenTracking is required for pageviews and for the leave events that carry per-screen time.

Backgrounding the app closes the current screen’s stint and force-flushes; time spent in the background is never counted toward it. If the app is killed with events still queued, they are replayed on the next launch under their original session id, so a crash does not silently eat the tail of a session. That requires a storage adapter — omit it and ids reset every launch.

Unity specifics

Single-mode scene loads become pageview: /scene-name; additive loads are ignored, since a streamed chunk or a UI overlay is not navigation. Clicks are raycast through the EventSystem to the nearest Selectable or IPointerClickHandler, which covers Canvas UI and nothing else — world-space objects and UI Toolkit need explicit Capture. Pointer travel beyond ~8dp is a drag, not a click.

A single-scene game gets exactly one pageview unless you call AgentHog.Screen("/shop") as UI panels change — worth doing, because screen paths are what funnels and time-per-screen are built from. Pausing or quitting emits leave and flushes; resuming after the idle window starts a new session and re-emits the current screen as its entry. Unsent batches survive to the next launch via PlayerPrefs.

Session context and auto-registered props

Once per session (and again whenever register() changes), the SDK sends a context blob: landing URL, referrer, screen size, viewport, timezone, and language. On mobile the landing URL is the deep link that launched the app when there is one — so install and campaign attribution rides the same pipe as a web ?utm_source= — and a synthesized app://name/path otherwise.

The mobile SDKs also register props onto every event, with no setup:

PropWhere it comes from
platformReact Native and Capacitor: ios/android. Unity: ios, android, webgl, standalone, editor
app_versionYour configured version (Unity defaults to Application.version)
os_versionPlatform version string
device_modelUnity only
engineUnity only — e.g. unity 2022.3.10f1

platform, app_version, and os_version read as bare dimensions in the CLI: ah events --by platform, no props. prefix needed.

What the server derives

None of this is sent by the SDK — it is computed at ingest and at sweep, and is what most dashboard breakdowns actually group on.

DerivedFrom
browser, os, device_typeThe User-Agent. Mobile SDKs send an identifying UA (myapp/1.0 AgentHogRN/0.3.0 (ios 17.4)) so they never look like an HTTP library
geo_country, asn, asn_nameAn ASN/geo lookup on the request IP — country granularity only
entry_path, referrer, utm_*, fbclid, gclidParsing the landing URL; the full entry query string is kept in landing_params
duration_s, pageviews, exit_path, max_scrollRolled up from the session’s own events
engaged, bounce, converted, is_returning≥30s or ≥2 interactions; ≤1 pageview and <10s with no interaction; any event matching a project goal; a prior session from the same visitor
classification, bot_scoreClient signals + datacenter ASN + crawler UA → human, unknown, suspected_bot, crawler, server, or test

Sessions and identity

A session rotates after 30 minutes idle on every platform, and the anonymous id persists across them — first-party cookie plus localStorage on web, your storage adapter in React Native, PlayerPrefs in Unity. There is no third-party cookie and no cross-site identifier anywhere in the stack.

identify(email) is what merges a person across devices. Games rarely have an email; a stable user_id trait stitches just as well. reset() at sign-out mints a new anonymous id and session so the next player is not attributed to the last one.

What is never captured

No field values, ever

Forms and inputs report field names only. Password and hidden inputs are skipped outright — not even their names are sent — so a submitted login form yields form_submit: login with fields: ["email"] and nothing else.

No raw IP addresses

The IP is used at ingest for geo/ASN lookup and then discarded. What is stored is a salted hash that rotates daily, so the same visitor’s sessions cannot be linked by IP across days — not by us, not by anyone with database access.

No replay, no keystrokes, no screenshots

There is no session-recording product hiding in the tracker. Keyboard activity contributes a single boolean used for bot scoring; nothing about what was typed leaves the device.

Whatever you put in props

The one place PII can enter is custom event props, which are stored verbatim and readable by anyone with dashboard access. Send ids and categories, not tokens, secrets, or profile blobs.

Extending it with custom events

Autocapture covers navigation and interaction. It cannot know that a checkout succeeded or a boss was defeated — that is what capture is for. Four calls, the same on every platform:

CallWhat it does
capture(name, props)A custom event. Name stored verbatim, props queryable
identify(email, traits)Attaches identity to this visitor and merges them across devices
tag(name, value)Sugar for a single trait; also emits tag: <name>
register(props)Props merged into every subsequent event — build channel, cohort. For A/B variants use real feature flags: flag() stamps $ff/<key> automatically

Web — the global exists only in the browser, only after the script runs:

window.agenthog.capture('checkout_completed', { plan: 'pro', cents: 4900 })
window.agenthog.identify('[email protected]', { plan: 'pro' })
window.agenthog.register({ variant: 'b' })

React Native — anywhere inside the provider:

const ah = useAgentHog()
ah.capture('photo_sent', { recipients: 3 })
ah.identify(email, { clerk_id: user.id })
ah.reset()                                   // sign-out

Unity — static calls, safe no-ops before init or when disabled:

AgentHog.Capture("level_complete", new Dictionary<string, object> {
    ["level"] = 12, ["deaths"] = 3, ["seconds"] = 94,
});
AgentHog.Screen("/shop");                    // in-scene UI states
AgentHog.Identify(traits: new Dictionary<string, object> { ["user_id"] = playerId });

Some events have no client to fire them — a subscription renews, a webhook lands, a nightly job decides someone churned. Those go straight to the ingest endpoint from your backend: server to server.

Naming rules that keep events queryable

  • · Values go in props, not in names. capture('plan_selected', { plan: 'pro' }) can be split with --by props.plan. plan_selected_pro is a name you will have to grep for forever.
  • · Keep the autocapture shapes. If you emit screen views by hand, emit pageview: /path. The CLI, funnels, and paths all parse the prefix: subject form.
  • · Short, readable, stable. checkout_completed, not evt_CHECKOUT_v2 and never a UUID. Names are the query surface — an agent composes questions from them without re-deriving your schema.
  • · Instrument outcomes, not every tap. Clicks are already captured. Custom events are for the things that mean something happened.

Reading it back

Everything above is queryable the moment it lands — the web tracker flushes every 5s or 10 events, React Native and Unity every 10s or 20 events.

ah events --since 24h              # everything, newest first
ah events top --by platform        # what is firing, split by platform
ah events --name checkout_completed --by props.plan
ah goals set signup "form_submit: waitlist"

Or open the dashboard. Not integrated yet? Install an SDK first — or point your agent at the skill and let it do the wiring.