Read this once before you write the code. The rules are the same for a website, an app and a static site: they are about how an event turns into a metric, not about the code that sends it. The call shape is in Counter calls.
Agree on the dictionary before the first call
An event name is not a label for yourself. It is the address the event is later counted by. Renaming after the fact tears the history apart: old records keep the old name, and the metric shows a gap where nothing changed.
Twenty names each invented on the spot are twenty metrics and not a single breakdown. Write the list of events before the code, and write it whole: which facts you intend to count, and which fields tell them apart.
Name the event the way an address is written
- Latin letters, lower case, words joined by
_. No colons in the name — the event will be recorded, but it cannot be counted as a metric. - Past tense,
what_happened:report_exported,plan_upgraded,checkout_completed. Notexport_report, notclick_button.
Put differences in the data, not in the name
lead_submitted with a product field — not lead_submitted_basic and
lead_submitted_pro.
Names that differ by a suffix cost you twice: every new plan needs a new metric, and a breakdown “by plan” never appears — a breakdown is built from a data field, and with no field there is nothing to build from.
The reverse holds too: something you will never compare against anything does not have to be a field.
Put the call on a confirmed fact, not on a click
- Saving, a status change, a payment — the event belongs in the success branch of the server’s answer. A rejection is not a user’s decision.
- A form success only JavaScript knows about is not a fact. The safer success is the state of the page after submission — what the person actually saw.
- Only what happened in a browser. A payment confirmed server-side or a nightly recalculation are not site events: they have no visit.
- An event fired while leaving the page may not make it. Count it “where possible” and put the reliable anchor on the receiving page: not “clicked checkout” but “opened the checkout page”. The gap between those two numbers shows itself in the report and tells the truth about delivery.
Guard anything that re-renders
KO does not deduplicate: one call, one record. A screen or page that re-renders itself — a waiting state, a redirect after a form — fires on every pass.
- Keep the “already counted” key in
sessionStorage: it survives a reload but does not travel to a new tab. - A key taken from the response (a record’s creation date, say) will not do: it survives a second attempt too, so the second attempt simply is not counted.
- What to check is not “are there duplicates at all” but “does the counter grow when the page is shown again”.
Reduce address values to a known list
Anything reaching event data from a query parameter should be mapped to a list:
var src = new URLSearchParams(location.search).get('from') || '';
var known = ['pricing', 'blog', 'email'];
track('signup_started', { from: known.indexOf(src) !== -1 ? src : 'other' });
Otherwise anyone opening the address with an arbitrary ?from= mints a new breakdown row,
and the breakdown stops being readable.
For the same reason, do not work out the section or page type in the browser by parsing
location.pathname if the site already does that at build time: a second copy of the URL
scheme drifts from the first silently, and in the report it looks like a page filed under the
wrong section.
Do not duplicate what KO fills in itself
Do not put source, campaign, device, city or the new-visitor flag into the context. They come from the visit, and a second copy inside event data will drift from the first over time.
The counter records page views itself as well, SPA routes included. Your own router subscription with its own “page view” doubles them rather than backing them up.
Related pages
- Counter calls — call shape, envelope, queue, options
- Custom events from a single-page app