Event Taxonomy

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

PrefixMeaning
$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 elseYour 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({...}).

EventFires whenProperties
$pageviewPage loads, history.pushState, history.replaceState, popstatetitle (document title)
$clickUser 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_submitA <form> is submittedform_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

EventFires whenProperties
$identifyYou 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.

EventFires whenProperties
message_impressionFirst step of a message renders (fired once per campaign, not per step)message_id, step_type (tooltip | modal | banner)
message_cta_clickUser clicks the CTA button on any step (terminal — flow ends)message_id
message_dismissUser closes via the × button or backdrop click (terminal — flow ends)message_id
message_completeUser 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

LimitValue
Events per batch500
Event-name length1–255 chars
Property value length (in trend filters)1024 chars
element_chain ancestor depth5
element_chain text snippet per element64 chars

Where events go

SDK → POST /e (ingest) → Kafka → ClickHouse `trackcrumb.events`

                          dashboard queries:
                            /trend, /funnel, /retention, /users

PII 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 labelProperty keySource
Country$countryTop-level country column in ClickHouse, enriched by the ingest service via MaxMind GeoLite2. Always populated when MaxMind DB is present.
OS$osproperties['$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$browserproperties['$browser']. Same as $os — customer-attached, not auto-enriched.
Custom…any stringproperties['<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.

  • Quickstart — get the SDK installed and emitting events
  • Funnels — how to use these events in funnel reports
  • Messages — what triggers message_* events