Analytics
Your app knows things you want to know: how many people signed up this week, which feature actually gets used, what the orders added up to. Analytics is how it tells you.
One line where the thing happens, and the number shows up in the Analytics tab of your project.
This is for you, not for the app
Section titled “This is for you, not for the app”Analytics events are write-only. There is no app endpoint that reads them back, and there will not be one.
If a screen inside your app needs a number — a total on a dashboard, a count next to a tab — that
is a Data collection and _aggregate, which is exact and instant. Analytics
are the slower, cheaper record that you read, in your own console.
Recording one
Section titled “Recording one”The app doesn’t write that fetch. It imports events, and the module sends it:
import { record } from "events";
await placeOrder(cart);record("order", { group: cart.plan, value: cart.total });record() answers nothing and never throws. There is no promise to await, no error to catch and
no response to read — it hands the event to the browser and returns on the same tick, so it can’t
slow a click down, can’t hold up a navigation, and a send that fails can’t take the screen down
with it.
Note the order: the event is recorded after the thing it records. Recording never delays what the person is waiting for, and a failure to record never fails their action.
It goes out through sendBeacon,
which the browser delivers in the background and doesn’t cancel when the page is closing — so an
event fired on the click that navigates away still arrives. That’s the send a hand-written fetch
loses.
Recording needs a signed-in caller — the dataset is yours, and a stranger who could write to it could both spend it and poison it. A signed-out visitor records nothing, and since nothing is awaited the app never hears about it, so no screen should wait on an event having landed.
The endpoint
Section titled “The endpoint”The module calls one route, and it’s the only one Analytics has:
| Method & path | Body | Returns |
|---|---|---|
POST /api/_events |
{"name":"signup","group?":"pro","value?":49} |
{"recorded":true} |
Your app never posts this itself. The table is here for server.mjs, which is uploaded as a lone
module with no module graph and so can’t import events — there, and only there, the route is
posted through env.PLATFORM.fetch().
A name, a group, a number
Section titled “A name, a group, a number”| Field | What it is |
|---|---|
name |
What happened. This is the thing that gets counted. |
group |
Optional. One dimension to break the total down by — the plan, the source, the kind. |
value |
Optional number to add up — an order total, a duration. |
record("signup"); // one thing happenedrecord("signup", { group: plan }); // broken down by one dimensionrecord("order", { group: plan, value: 49 }); // and a number to add upKeep the name fixed. "signup", "order", "export" — short, and always the same string. A
name that carries a changing value ("signup-sam@acme.com") makes a million different events and
counts nothing. That is what group is for.
One call is one event: the route takes one at a time, so there is no batch to build and no buffer to flush. And because nothing is waiting on the answer, the module settles a bad event itself — a name that is missing, blank or over 64 characters records nothing at all, a group is trimmed to 64 characters and left off when it’s empty, and a value that isn’t a finite number is left off while the event still records.
What not to record
Section titled “What not to record”No email addresses, no names, no ids, no free text somebody typed. A group is a category, not a
person.
Do not record every page view or every keystroke either. Each call spends your own Cloudflare account, and a caller is capped at 300 events a minute. Record the handful of things you would actually ask about.
What the numbers mean
Section titled “What the numbers mean”The Analytics tab shows the last 30 days from your deployed app, one row per event name with its
groups underneath. A preview records nothing and answers {"recorded":false}, so test traffic
never mixes with the real numbers.
At high volume Cloudflare samples the stream and scales the counts back up. A large number is a good estimate, not an exact tally — which is fine for “how are we doing”, and the reason you should never show one to a customer as if it were a balance.
Where they live
Section titled “Where they live”In an Analytics Engine dataset in your own Cloudflare account, alongside the errors your deployed app reports. Typillar reads them back to draw your Analytics tab and stores nothing itself.