Docs
This is what's actually running.
Writing docs for something nobody can sign up for is a slightly odd exercise, I'll admit. But the shapes below are real: typed, tested, and taking events today. The missing part is the one where you get an account.
First up: what a browser sends
Your visitor's browser sends this to a path on your own domain, and your proxy passes it along. Short and boring, which is what you want from something that runs on every page view.
{
name: "pageview", // lower_snake_case
props: { // optional, up to 16
plan: "pro"
},
ctx: {
path: "/pricing",
route: "/pricing", // optional: your framework's pattern
ref: "google.com" // optional
}
} There's nothing in there naming which app it came from. That's on purpose. See below.
And what your backend sends, which is different
If the event happened somewhere a browser can't see — an import finished, a subscription lapsed — your server sends it instead, and it can tell me who it was for. Your server has verified that person's token, which a browser can't do for itself. So the browser shape has no field for it.
{
name: "import_completed",
ctx: { route: "/unit/[unitName]" }, // the template, not the real path
subject: { // required here, forbidden above
iss: "https://you.auth0.com/",
sub: "auth0|653f..."
},
client: { // optional
ua: "Mozilla/5.0 (iPhone; CPU..." // the browser that caused this
}
} That subject gets hashed the moment it arrives and is never written
down anywhere. Note route rather than path: your real URLs probably have customer names in them, and I'd
rather not have those.
client.ua is how device and browser end up on a server event. Send
the user agent from the request that caused it — the one your user's browser made to you, not the one your HTTP library sends me. I read it, work out desktop or phone and
which browser, and drop the string. If a cron job or a webhook triggered the event and there was
no browser involved, leave it out.
There's one secret, and the browser never sees it
Every request carries a secret header. Your edge injects it, or your backend holds it. The browser never gets to see it, which means there's nothing on the client worth stealing.
That header is what says which tenant and which app this is, which is why there's no app field in the event itself. If a browser could set it, anyone could
send events as anyone else.
Two things there's no field for
There's no field for who the visitor is, and none for when it happened. The schema has nowhere to put either one, so there's nothing for me to check and reject.
Anonymous visitors get an identifier I work out on my side. Timestamps come from my clock, not yours, because client clocks are wrong, skewed, or occasionally lying.
Naming things, the second hard problem
Names are lower_snake_case. Register the ones you want counted;
anything else still gets stored, it just doesn't get its own running total. Registering is what
keeps the dashboard fast and the bill small.
Query strings get stripped on the way in. If one of your URLs has a password reset token in it, I don't keep it.
Paths themselves I leave alone. /user/1 and /user/2 are two different pages as far as I know, and if you want them
counted as one, that's what ctx.route is for. Your framework knows
the pattern. I'd only be guessing.