# What gets tracked > The anatomy of an OakData event: system vs custom events, the properties attached to every event, and context. Source: https://oakdata.co/docs/concepts/events --- Everything OakData records is an **event**: a named action with properties, stamped with who did it, when, and in which session. Events come from two places - [autocapture](https://oakdata.co/docs/sdk/autocapture) and your own `oak.capture()` calls. ## Anatomy of an event The fields outside `properties` identify the actor and session; everything specific to the event lives inside `properties`. **event.json** ```json { "id": "evt_5f2c…", "event": "signup_completed", "timestamp": "2026-06-11T14:03:21.881Z", "distinct_id": "user_8f3a", "anonymous_id": "a3c1…", "session_id": "s_a1b2c3", "session_number": 2, "session_started_at": "2026-06-11T13:58:10.020Z", "properties": { "plan": "pro", "$pathname": "/welcome" }, "context": { "library": "oakdata-js", "library_version": "1.0.0", "page": { "url": "...", "path": "/welcome", "title": "Welcome", "referrer": "" } }, "traits": { "email": "sam@acme.com" }, "groups": { "company": { "id": "acme-co", "traits": { "name": "Acme" } } } } ``` ## System vs custom events Names starting with `$` are **system events** emitted by the SDK - `$pageview`, `$click`, `$form_submit`, `$identify`, `$set`, `$group`, and so on. Your own events use plain names (`signup_completed`, `invoice_paid`). The same `$` convention applies to system *properties*. > **Naming convention** Use `snake_case`, past-tense, object-then-action names (`checkout_completed`, `file_uploaded`). Keep high-cardinality values (ids, emails) in properties, not in the event name. ## Properties attached to every event The SDK enriches each event with context the server stores as first-class columns - filter and group without parsing JSON. Common keys: | Name | Type | Description | | --- | --- | --- | | `$current_url / $pathname / $host / $search` | string | The page URL and its parts. | | `$title` | string | The document title. | | `$referrer / referring_domain` | string | Where the visitor arrived from. | | `browser / browser_version / os / os_version` | string | Parsed from the user agent. | | `device_type` | string | desktop, mobile, or tablet. | | `viewport_width / viewport_height` | number | Browser viewport size. | | `utm_source / utm_medium / utm_campaign / utm_term / utm_content` | string | Campaign attribution, captured from the landing URL and carried via first/last touch. | Geo fields (`country`, `region`, `city`) are derived **server-side** from the request IP at ingest - the SDK never sends them. ## Custom properties Pass any JSON-serializable object as the second argument to `capture`. Keep property names stable, and values low-cardinality where you intend to group by them. **custom** ```ts oak.capture('plan_changed', { from: 'free', to: 'pro', annual: true, seats: 5, }) ``` > **Scrubbing sensitive data** Strip fields before they leave the browser with the `property_denylist` init option, or rewrite/drop whole events with `before_send`. See the [SDK reference](https://oakdata.co/docs/sdk/reference).