Agent-native analytics

Analytics an agent can install, and an agent can read.

One command wires StepMetrics into your project. It finds your framework, puts the tracker in the right file, and writes the SDK bootstrap if your code talks to a model. After that your agents report what they did and query the results over MCP, and your web traffic lands in the same database.

npx stepmetrics init --endpoint https://app.stepmetrics.co --site YOUR_SITE_ID

Free while it is in early access. One Fastify and Postgres service, and you can run it yourself.

Step one: install

What the one command does

Hand it to your coding agent or run it yourself. It reads the project, decides where the tracker belongs, and edits that file. Running it a second time changes nothing.

It knows your framework

Next.js App Router gets a next/script tag in the layout. Astro, Vite and plain HTML each get the snippet in the file that actually renders on every page.

It wires the agent SDK

If your package.json pulls in an AI library, it writes a stepmetrics.ts bootstrap for you to import where your agent runs.

It refuses to guess

If it cannot identify the project, it prints the snippet and exits non-zero. A tag written somewhere plausible and wrong shows up as missing data weeks later.

Step two: report

Your agents report what they did

Runs, model calls, tool calls, tokens and cost. Wrap an existing Anthropic client and every call is recorded with the provider's own token counts, without touching a single call site.

import Anthropic from "@anthropic-ai/sdk";
import { instrumentAnthropic } from "@stepmetrics/sdk";

const run = analytics.startRun({ name: "triage" });
const client = instrumentAnthropic(new Anthropic(), run);

Cost is priced on the server

Send tokens, not money. A model with no pricing row is recorded as unknown cost instead of zero, and the ingest response tells you which, so an undercount is visible while you can still fix it.

Retries cannot double-count

Span keys are generated per run, so a replayed run regenerates the same keys and the server writes over the originals. Pass your own run key to make that hold for anything nondeterministic.

Ingest keys are write-only, and they are their own scope rather than a weaker read key. A leaked ingest key can add telemetry and do nothing else.

Step three: query

Your agents read it back over MCP

Ten read-only tools, so an agent can ask what happened instead of waiting for someone to export a CSV.

claude mcp add --transport http analytics https://app.stepmetrics.co/mcp \
  --header "Authorization: Bearer dak_..."

Same query functions as the dashboard

The MCP tools call the dashboard's own functions. An agent and the dashboard cannot disagree about what "visitors" means, and a test checks that both report the same number for the same segment.

Nothing writes

None of the ten creates, deletes or mints anything, and they take a read-scoped key. "The model would not do that" is not a security control.

And the web side

It is a real web analytics tool too

Which pages get traffic, where it came from, which countries, devices and browsers, where people landed and left, how many are on the site right now, and how all of that compares with last period. Then how many of them downloaded, started checkout, and bought.

Segments compose

Narrow any number by country, device, browser, OS, source or entry path. "Only mobile visitors from Hacker News" is one query string, and it narrows the funnel too.

Funnels

Ordered conversion steps. Your segment filters who enters the funnel, not only who reaches step three, so the rate you read belongs to the people you asked about.

Bot traffic stays visible

Filtered traffic is kept and marked. One parameter shows you exactly what the filter took out, so you can check whether it was right.

How it works

How a visitor gets counted

A visitor id is a hash of four things: a daily salt, the site, the IP address and the user agent. The IP and the user agent go into the hash and are then thrown away. Neither one is written to any table, and there is no cookie banner because there is no cookie.

visitor_id = base64url(sha256(daily_salt || site_id || ip || user_agent))[:22]

The salt rotates at UTC midnight

The same person gets a different id tomorrow. Old salts are deleted, and once they are gone nobody can recompute a historical visitor id, including someone holding the whole database and a suspect's IP address. Deleting the salt is the privacy control.

One service, one database

No third party ever sees a request, and no visitor is handed a cookie or any other durable identifier. The tracker honours Do Not Track and Global Privacy Control, and writes nothing to localStorage.

Honest limits

What you give up

Each of these follows from not storing a durable per-person id. The API responses and the dashboard say so too, so you will run into them inside the product as well.

  • Visitors is a distinct-per-day count. Someone who visits Monday and Tuesday counts twice over a 30-day window. A true "unique visitors over 30 days" number needs a cookie.
  • Funnels cannot cross UTC midnight. The window caps at 24 hours, and a longer one is rejected outright instead of quietly clamped. For conversions that really happen days later, attribute from billing records, where the durable identifier is the order.
  • Geography stops at country. A city plus a timestamp starts to identify individuals, so there is no city setting to switch on by accident.
  • There is no session replay, no user profiles and no cohort retention. All three need the identifier this design refuses to store.
Contact

Ask us something

This form posts to the analytics server, which delivers it to an inbox held in server-side configuration. No address appears on this page, so there is nothing here for a scraper to collect.