What’s new

What we shipped, newest first — one post per feature, and what it means for you. For SDK version numbers and changes that move data you have already looked at, read the SDK changelog.

Subscribe by RSS

Reports

Publish a report your whole team opens

A Markdown file with live charts in it — you write the analysis once, the numbers keep themselves current.

The weekly numbers used to be a ritual: run ah digest, paste the interesting rows into Slack, write two sentences of context around them. By Tuesday the numbers were wrong and the two sentences were the only part still worth reading.

A report inverts that. It is an ordinary Markdown file where a ```widget fence stands in for a chart, a table, or a stat tile. Your prose is frozen — the judgement you made on Monday stays exactly as you wrote it. Each widget carries a constrained SQL query and a TTL, so it recomputes against the account's live data instead of aging into a lie. Publish it and the whole organization reads it at /reports/<slug>.

Revisions are append-only. history, pull --rev and diff reach any earlier version, so last month's read is still there next to the numbers it was based on — which is the part that actually makes a report worth writing.

There is an agent skill for the whole loop, too: /skill/report/SKILL.md.

$ ah reports create weekly-review --file weekly-review.mdShared reports

Attribution

Singular postbacks land server to server

Point Singular's Internal BI postback at AgentHog and installs arrive attributed, with no client code involved.

Mobile install attribution lives inside your MMP, and your product analytics lives here. Reconciling the two by hand — exporting campaign names, matching them to sessions on a guess — is the kind of chore that quietly never gets done.

POST /postbacks/singular receives Singular's Internal BI postbacks directly. You configure the endpoint once in project settings, Singular delivers each install verdict server to server, and the campaign lands on the person's session utm_* columns. No SDK call, no app release, and nothing that can be dropped by an offline launch.

Precedence is explicit, per column: a real deep-link value beats the MMP verdict, which beats the install-referrer stamp. An organic answer never erases a referrer that already had a real one. Which means every campaign report — traffic, funnels, revenue by source — just starts including paid installs.

Prefer to attach the verdict from the app instead? The SDKs have setAttribution.

Platform

AgentHog moved to agenthog.io

New home for the product, the docs, the tracker and the API — with the old host still serving every existing install.

AgentHog was living at hog.brightmotion.io, which is a fine internal hostname and a poor thing to say out loud. The product now lives at agenthog.io: dashboard, docs, /ah.js, /api/v1, and the sign-in flow.

Nothing you have installed needs to change. The old host keeps serving /api/v1 indefinitely, existing CLI logins keep the host their token was minted against, and AH_API_URL still overrides everything. ah 0.10.0 makes https://agenthog.io the default for a fresh AH_TOKEN or a first-time ah login — upgrade when it suits you.

The one thing worth doing at some point: repoint your <script src=…/ah.js> tag. It works either way, but a snippet that names the product is easier for the next person to recognise.

Platform

Watch your events arrive, live

A running tail of raw events at /events — the page that answers "is my event firing, with the props I expect?"

Every integration has the same first ten minutes: you add the snippet, click the button, and then stare at a dashboard that aggregates on a delay, trying to work out whether the silence means "not wired up" or "not there yet".

/events is the tail. Trigger something in your app and watch the row land — event name, person, and the full prop payload expanded inline. Server-session events are visible by default here, because webhook relays and backend sends are exactly what people are usually debugging.

It reads the same query as ah events, so the page and the CLI can never disagree with each other about what arrived. It refreshes every 15 seconds — fast enough for the trigger-and-watch loop, and it holds no connection open, so nothing to reconnect when your laptop sleeps.

$ ah events --since 30mEvents
SDKs

Attach your MMP's verdict from inside the app

setAttribution in React Native 0.8.0 and Capacitor 0.4.0 forwards an attribution callback into the session, exactly once.

If you already have Singular's SDK in the app, its attribution callback fires with the answer you want. Getting that answer into your analytics used to mean sending a custom event and then reverse-engineering campaign reports out of it.

AgentHog.setAttribution({ provider, utm_source, … }) forwards the payload into the session's utm_* columns on the next flush's normal cadence. The hard parts are handled: MMP callbacks re-fire on every launch, so each distinct payload is delivered exactly once and the repeats are no-ops; an undelivered payload survives a crash or an offline launch and rides the first flush of the next one. reset() clears it.

A real deep-link value still wins over the verdict, and passing utm_source: "organic" is safe — it won't overwrite a referrer that already knew better.

Available in @brightmotion/agenthog-react-native 0.8.0 and @brightmotion/agenthog-capacitor 0.4.0. Prefer no app release? Use the Singular postback.

Attribution

One name per ad network, and first touch on every person

utm_source is normalized at ingest, so Meta stops showing up four times — and every person now carries a first and latest touch.

The same network arrives spelled differently depending on which pipe it came down. Meta alone turned up as Facebook, fb4a and apps.facebook.com — three rows in a source report, one actual ad network, and a top-channels list you had to mentally re-add.

New sessions now store one canonical name (meta, google, tiktok, …). Unknown values pass straight through unchanged, and the raw value is kept in landing_params, so nothing is lost. Historical rows are not rewritten: a report spanning this deploy shows a seam at that date, with old and new spellings side by side.

On top of that, attribution is now a property of a person, not just a session. ah user derives first_touch and latest_touch from their session ledger, and ah users --source filters people by where they originally came from — which is the shape you want for "how does the cohort from this channel actually behave".

$ ah users --source metaMobile attribution
CLI

Send events from the terminal

ah track writes a single event or bulk-imports a CSV — for backfills, cron jobs, and anything with no SDK.

Not everything that matters happens in a browser or an app. A refund processed in an admin tool, a nightly job that closes out trials, three years of history sitting in a CSV export from the thing you're migrating off — none of it has an SDK to call.

ah track <event> writes one event. --person and --email tie it to someone, --prop k=v sends bare numbers and booleans as JSON types (while 0022 and 1.9.2 stay strings, which is nearly always what you meant), and --at accepts a date or a wall-clock time and echoes back the exact instant it resolved to and the rule that fired.

ah track --file events.ndjson — or a CSV with --event-col, --ts-col, --person-col, --prop-cols — imports in bulk, packed under ingest's batch caps. --dry-run prints the row count, date range, event names and a sample payload without sending anything, and bad rows are reported by line number rather than half-imported.

One honest caveat, which the verb tells you itself: ingest has no idempotency key. Run an import twice and you have written it twice.

$ ah track signup --person u_123 --prop plan=proThe ah CLI
CLI

ah sql is now a contract, not a database connection

A published queryable surface, portable helper functions, bound time windows, and per-query ceilings.

ah sql let you ask anything, which sounds generous until you build something on a column that was never meant to be public and it moves under you. Worse, half of what you'd write was Postgres-specific — fine today, and a rewrite the day the storage engine changes.

ah schema now prints the promise: the readable tables, their documented columns, the helper functions, and what is guaranteed to keep working. Only what it lists is promised — internal columns (IP hashes, user agents, bot signals, raw landing params) are no longer readable at all, so SELECT * on those tables fails on permissions rather than silently handing you data you shouldn't build on.

The helpers are the portable ones you actually reach for: prop, prop_num, has_prop, match, median, p90/p95/p99. --since/--until fill :since and :until as bound timestamps resolved server-side, and constrained sessions are pinned to UTC, so date_trunc('day', …) buckets the same way regardless of where the query ran.

Queries run under stated ceilings — 10s statement timeout, 32MB work_mem, 60/min per token — and exceeding one returns an error that names the limit instead of a mystery hang.

$ ah schemaThe ah CLI
Platform

Install AgentHog into Claude Code as a plugin

One marketplace command installs all three agent skills — integrate, experiment, report.

The skills were already there — Markdown instruction files served raw from /skill/, meant to be dropped into ~/.claude/skills/. That works, and it's one more curl to explain in every onboarding conversation.

There is now a Claude Code plugin. /plugin marketplace add AnniesAI/agenthog-claude, then /plugin install agenthog@agenthog, and your agent has all three: how to integrate AgentHog correctly into whatever repo it's looking at, how to run an A/B test end to end without fooling itself about the result, and how to write and maintain a shared report.

Not using Claude Code? The curl one-liners still work, and every skill is a plain Markdown file any agent can read. That's the whole point of the format.

Platform

See how your account actually uses the API

Per-token, per-endpoint usage for the CLI and HTTP API, plus a monthly event meter against your plan.

Tokens accumulate. A CI job here, an agent there, a script someone wrote in March — and no straight answer to "which of these is still in use, and what is that nightly job doing to my event count?"

Your account's API usage is now recorded and readable: which tokens are calling, which endpoints, how often. Revoking a token stops being an act of faith.

Separately, /usage shows monthly event volume against your plan's allowance, drawn from a maintained rollup rather than counting the raw events table — so the page loads instantly and the number is at most a few minutes stale. It is monitoring only: nothing is enforced, and the meter is there so a surprise is something you notice in week one rather than on an invoice.

$ ah usageThe ah CLI
Platform

One person, however they typed their email

Emails are normalized at ingest, so [email protected] and [email protected] stop being two customers.

Someone signs up as [email protected] on the marketing site and logs in as [email protected] in the app. Two identities, two retention curves, one real person — and an LTV number quietly split in half.

Emails are now lowercased at ingest before identity resolution, so the case someone happened to use never decides whether their sessions stitch together. It applies to every path that carries an email: the SDKs, server-side sends, and ah track --email.

Small change; it shows up in every person-level number you have.

Economy

Track your game's virtual currency without instrumenting anything new

Declare which events source or sink currency and get flows, a paid/earned split, ranked sources and sinks — retroactively.

Soft-currency inflation is the failure mode nobody sees coming: the balance curve drifts up for six weeks, the store stops mattering, and by the time it shows in revenue you're retro-fitting a sink into a live game. Catching it early normally means designing an economy taxonomy up front and re-instrumenting the client to match.

It doesn't here. If your purchase and reward events already carry an amount, you map them from the CLI: ah economy map --event <name> --source|--sink. Conventional props (amount, currency, balance) are the defaults and get resolved at map time. Mappings are stored in project settings and applied retroactively over all history — no SDK change, no migration, no waiting a month for data.

The report prints one block per currency, never summed across them: sourced with a paid vs earned split, sunk, net flow with supply growth, and ranked TOP SOURCES / TOP SINKS with event and player counts. --by <dim> breaks flows down by platform, country or cohort; --series adds a per-day table.

And an always-on HEALTH line reports unparseable amounts, sign disagreements, and drift between your flows and the client-reported balance — which usually finds the instrumentation bug before it finds the design one. ah economy suggest proposes mappings from events you're already sending, so the first run takes about a minute.

$ ah economy map --event coins_spent --sinkIn-game economy