Event naming & tracking plan
How to name events, which names the SDK reserves, and how to enforce a typed tracking plan on the client before bad data reaches your warehouse.
The names you choose become your analytics schema, and they are far harder to change later than they are to get right now.
$ is reserved#
Every event the SDK emits itself begins with $. Tracking one of those names
yourself collides with the SDK's own event and corrupts both.
The complete list is generated from the source — see the reserved events reference. It covers, broadly:
| Group | Examples |
|---|---|
| Identity | $identify · $group · $alias |
| Autocapture | $pageview · $pageleave · $autocapture · $submit · $change · $impression · $outbound_link · $download |
| Signals | $rageclick · $deadclick · $exception · $web_vital · $exit_intent |
| Forms | $form_start · $form_abandon · $form_error |
| Flags | $feature_flag_called |
| Push | $push_opened |
| Inbox | eleven $message_* events |
| Coachmarks | twelve $coachmark_* events |
Internal $ events are never governed by a tracking plan — they are the SDK's,
and it is responsible for them.
Naming your own#
Pick one convention and hold it. The SDK does not enforce a style, so consistency has to come from you:
object_action, past tense —cart_viewed,checkout_started,subscription_cancelled. Reads well when the list is sorted alphabetically, because everything about carts groups together.Lower snake case. Mixed casing produces two events that look identical in a dashboard and are not.
Properties for variants, not names.
plan_upgradedwith{ from: 'free', to: 'pro' }, neverfree_to_pro_upgraded— otherwise every new plan needs a new event and every funnel needs rewriting.Do not put ids in names. They belong in properties.
Web and mobile do not share names#
This is the trap that costs the most time. The web SDK emits $pageview; the
mobile SDK emits screen_viewed. The vocabularies are genuinely different,
because the platforms are.
Anything that matches on a literal event name is therefore platform-specific. Cross-platform analysis goes through the backend's event-concept map, which is what makes "a view" mean the same thing on both — a dashboard query that hardcodes one platform's name silently reports zero for the other.
Tracking plan#
A typed plan is enforced on the client, before the event is sent, so bad data never leaves the browser:
await ScaleBun.init({ clientKey, apiBaseUrl, analytics: { plan: { events: { checkout_started: { required: ['cart_value', 'currency'], props: { cart_value: 'number', currency: 'string', coupon: 'string' }, }, }, allowUnknownEvents: true, // default — warn, never drop strict: false, // set true to DROP violations }, },});The plan catches three things: an event not declared in events, a missing
required property, and a property whose type does not match.
| Mode | Behaviour |
|---|---|
| Default | Warns in the console. Nothing is dropped. |
strict: true | Drops the violating event from both lanes. |
allowUnknownEvents: false | Treats an undeclared event as a violation. |
Super-properties and identity#
Properties registered with register() are merged into every subsequent event,
and call-site properties win on conflict. They are persisted, so they survive a
reload — and they are cleared by reset(), which you should call on logout so
the next user's events are not attributed to the previous one.
ScaleBun.register({ plan_tier: 'enterprise' });ScaleBun.unregister('plan_tier');Flag exposures are stamped the same way: reading a flag registers
$feature/<key>, so every downstream event carries the variant a user saw
without a join.
Next#
Product analytics — track, identify, group, revenue.
Reserved events reference — the generated list.