Event Taxonomy
Authoritative list of events that flow through TrackCrumb. The schema accepts any non-empty string as an event_name, so this is convention, not a closed enum — but the names below are reserved by the SDK and dashboard. Don’t reuse them for your own events.
Naming conventions
| Prefix | Meaning |
|---|---|
$ | Reserved by TrackCrumb — emitted by the SDK or used internally by the dashboard. Don’t emit your own $-prefixed events. |
message_ | Reserved by the in-app messaging feature (tooltips/modals/banners). |
| anything else | Your custom events. Recommended style: lowercase snake_case, verb-noun (signup_started, invoice_paid). |
Reserved property keys also use the $ prefix (e.g. $user_id, $client_x). Custom property keys should not start with $.
SDK-emitted events
Autocapture
Sent automatically when autocapture !== false is passed to new TrackCrumb({...}).
| Event | Fires when | Properties |
|---|---|---|
$pageview | Page loads, history.pushState, history.replaceState, popstate | title (document title) |
$click | User clicks an element matching a, button, [role=button], [data-track] | tag, href (links only), $client_x, $client_y, $viewport_width, $viewport_height, plus element_chain (CSS-selector breadcrumb up to 5 ancestors) |
$form_submit | A <form> is submitted | form_id, form_action, plus element_chain |
To opt out, pass autocapture: false to the constructor. To capture clicks on a non-default element, add data-track to it.
Identity
| Event | Fires when | Properties |
|---|---|---|
$identify | You call tracker.identify(userId, traits) | $user_id plus any traits you pass. Subsequent events on the same browser use userId as distinct_id. |
Call identify() as early as possible after login so anonymous → signed-in journeys join up across the same browser session. Without it, the same user’s pre-login and post-login events count as two different distinct_ids in funnels and retention.
In-app messages
Emitted by the SDK when a message campaign (created in the dashboard Messages page) renders on the user’s page.
| Event | Fires when | Properties |
|---|---|---|
message_impression | First step of a message renders (fired once per campaign, not per step) | message_id, step_type (tooltip | modal | banner) |
message_cta_click | User clicks the CTA button on any step (terminal — flow ends) | message_id |
message_dismiss | User closes via the × button or backdrop click (terminal — flow ends) | message_id |
message_complete | User clicks Done on the final step of a multi-step campaign (terminal — flow ends successfully) | message_id |
Terminal events (message_dismiss, message_cta_click, message_complete) are written to both the ClickHouse events table (via the standard ingest pipeline) and the Postgres message_impressions table (via POST /api/v1/messages/:id/track). The Postgres write powers server-side frequency-cap filtering on subsequent /active fetches.
Dedupe key in Postgres message_impressions is (message_id, distinct_id, session_id, event) — one row per session per user per event type.
Custom events
Anything else you send via tracker.track(name, properties). Keep names stable and snake_case so the dashboard’s funnels/trends/retention can match on exact event_name.
tracker.track("signup_started");
tracker.track("invoice_paid", { amount_usd: 49, plan: "growth" });All property values are coerced to strings in transport — numbers, booleans, dates → String(value).
Event payload shape
Every event sent over the wire has this shape:
{
distinct_id: string; // userId from identify(), else anonymous browser id
session_id: string; // 30-min idle-timeout session id
event_name: string; // e.g. "$pageview" or "signup_started"
timestamp: string; // ISO-8601, set client-side
properties: Record<string, string>;
element_chain: string; // autocapture only; "" otherwise
url: string; // location.href at time of capture
referrer: string; // document.referrer
user_agent: string; // navigator.userAgent
}After server-side validation and PII scrubbing, events land in ClickHouse table trackcrumb.events.
Limits
| Limit | Value |
|---|---|
| Events per batch | 500 |
| Event-name length | 1–255 chars |
| Property value length (in trend filters) | 1024 chars |
element_chain ancestor depth | 5 |
element_chain text snippet per element | 64 chars |
Where events go
SDK → POST /e (ingest) → Kafka → ClickHouse `trackcrumb.events`
↓
dashboard queries:
/trend, /funnel, /retention, /usersPII scrubbing runs server-side on the ingest path (regex denylist for email, SSN, credit-card, E.164 phone, street addresses).
Segment breakdown property-key convention (experiments)
The experiments results panel supports a “Break down by” selector that groups conversion data by a property value. The following standard keys are offered as quick-filters:
| Dashboard label | Property key | Source |
|---|---|---|
| Country | $country | Top-level country column in ClickHouse, enriched by the ingest service via MaxMind GeoLite2. Always populated when MaxMind DB is present. |
| OS | $os | properties['$os']. Not auto-enriched by the ingest service — customers must attach it manually: tracker.track(event, { $os: "iOS" }). The SDK does not auto-parse the user-agent into this key. |
| Browser | $browser | properties['$browser']. Same as $os — customer-attached, not auto-enriched. |
| Custom… | any string | properties['<your_key>'] for any arbitrary property you emit via tracker.track(). |
Note: The ingest service parses the user_agent string server-side (uap-go library) and
stores the parsed OS/browser in structured Go variables used for logging, but does not
write them back into the ClickHouse properties Map. If you want OS/browser segments to
work out of the box, attach $os and $browser on every track call from your code.
A future ingest enrichment pass could write $os/$browser into properties automatically;
until then, the $country segment is the only one that works without customer instrumentation.
Related
- Quickstart — get the SDK installed and emitting events
- Funnels — how to use these events in funnel reports
- Messages — what triggers
message_*events