> For the complete documentation index, see [llms.txt](https://docs.millimetric.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.millimetric.ai/core-concepts/properties.md).

# Properties

`properties` is a free-form JSON object you attach to any event. It's where the *interesting* part of an event lives — the plan, the amount, the button label, the experiment variant.

```json
{
  "event": "purchase",
  "properties": {
    "amount_cents": 4900,
    "currency": "usd",
    "items": 2,
    "plan": "pro",
    "discount_code": "SPRING25"
  }
}
```

Limits:

| Limit                             | Value                                                                             |
| --------------------------------- | --------------------------------------------------------------------------------- |
| Total size after `JSON.stringify` | **8 KB**                                                                          |
| Property names                    | strings, no length cap, but be reasonable                                         |
| Property values                   | any JSON-serialisable value — string, number, boolean, null, array, nested object |
| Cardinality                       | no server-side cap (your problem if you put `request_id` on every event)          |

If `properties` exceeds 8 KB, the value is **replaced** by `{"__truncated":true,"original_bytes":N}` rather than partially committed. We never ship a malformed payload.

## What you put there

Anything specific to *this* event that you want to slice on later.

```ts
track("video_started", {
  video_id: "v_42",
  duration_s: 320,
  quality: "1080p",
  player: "native",
  is_autoplay: false
});

track("checkout_completed", {
  amount_cents: 4900,
  currency: "usd",
  item_count: 2,
  payment_method: "card",
  is_first_purchase: true,
  experiment_variant: "control"
});

track("error_shown", {
  error_code: "RATE_LIMITED",
  http_status: 429,
  retry_count: 2,
  endpoint: "/v1/track"
});
```

Rules of thumb:

* **One value per key.** Don't pack JSON into a single string property — the query layer can't index strings as objects.
* **Numbers as numbers, booleans as booleans.** `amount_cents: 4900`, not `"4900"`. `is_first_purchase: true`, not `"true"`.
* **Money in cents.** Or a fixed-precision integer. Float arithmetic on revenue is a path to bug reports.
* **Don't double-encode.** ClickHouse stores `properties` as a `String`; the API parses your object once on insert and re-serialises it. Sending `properties: "{\"plan\":\"pro\"}"` makes querying painful.

## What you don't put there

| Don't put in `properties`                               | Why                                                                     | Use instead                                    |
| ------------------------------------------------------- | ----------------------------------------------------------------------- | ---------------------------------------------- |
| Personally identifying info (email, full name, address) | Privacy. Once it's in events, it's hard to clean.                       | Keep PII in your own DB; key by `user_id`.     |
| Free-text user input                                    | Cardinality explosion.                                                  | Hash it, bucket it, or skip it.                |
| Raw IP, exact location                                  | We HMAC IPs and store country only — sending raw IP defeats the design. | Trust the server's `country` enrichment.       |
| Secrets / tokens                                        | They'd live forever in ClickHouse.                                      | Anything. Just not this.                       |
| Huge blobs                                              | 8 KB cap.                                                               | Store in your own object storage; pass the id. |

## What the browser SDK adds for free

On every event, [`@millimetric/track`](/sdks/browser.md) merges these into `properties`:

```ts
{
  utm_source, utm_medium, utm_campaign, utm_content, utm_term,
  fbclid, gclid, ttclid, msclkid, li_fat_id, dclid, gbraid, wbraid, yclid,
  $viewport_w, $viewport_h,
  $language,           // navigator.language
  $timezone            // Intl.DateTimeFormat().resolvedOptions().timeZone
}
```

Plus on the event itself (not in `properties`):

```ts
{ url, path, referrer, anonymous_id, user_id, timestamp }
```

The classifier reads `url` + `referrer` to produce `source / medium / campaign / source_confidence / source_rule_id` server-side — see [Attribution](/core-concepts/attribution.md).

You can override anything by passing it explicitly:

```ts
track("custom_pageview", {
  utm_source: "newsletter",     // overrides the URL-derived one
  utm_medium: "email"
});
```

## What the Node SDK adds

Almost nothing. The Node SDK is a thin wrapper over the HTTP API — it doesn't infer browser-only context (no UA, no language, no viewport). You send what you mean.

What you *do* get for free, server-side:

* `country` from the request IP via Cloudflare geo headers.
* `device_type / browser / os` from the User-Agent header — though for backend events that's normally "the request that triggered this server-side `track`", which may not match the visitor's actual browser. Pass `url` + `referrer` from your request handler if you want the classifier to fire correctly.

## Naming conventions

Pick one and stick to it. We recommend:

* **snake\_case** for property names: `amount_cents`, `is_first_purchase`, `plan_tier`.
* **`$`-prefix for system properties** the SDK or server adds: `$viewport_w`, `$language`, `$timezone`.
* **Units in the name** when ambiguous: `duration_ms`, `amount_cents`, `size_bytes`, `latency_ms`.
* **Booleans named as questions**: `is_paid`, `has_premium`, `was_invited`.
* **Currency always alongside amount**: `amount_cents` + `currency: "usd"`.

Full conventions in [Event & property naming](/reference/event-naming.md).

## Property semantics that downstream tools rely on

Some properties are special because the API or MCP tools index on them.

| Property                                 | Where it surfaces                                                | When to set it                                                          |
| ---------------------------------------- | ---------------------------------------------------------------- | ----------------------------------------------------------------------- |
| `utm_source / utm_medium / utm_campaign` | Drives `source / medium / campaign` if `url` doesn't carry them. | Pass-through from server-side handlers that already parsed the URL.     |
| `$pageview` (event name, not property)   | Drives `path`, page-view counts, top-paths breakdown.            | Browser SDK handles automatically; call `page()` for virtual pageviews. |
| `amount_cents` + `currency`              | Convention for the upcoming revenue dashboards.                  | On any conversion event with monetary value.                            |
| `experiment_id` + `variant`              | Convention for A/B test analysis.                                | On every event that occurred under an active experiment.                |

None of these are *required* — but if you set them with these names, the dashboards and the MCP `top_sources` / `get_stats` tools will Just Work.

## Querying on properties

`properties` is a `String` in ClickHouse holding JSON. To filter or aggregate on a property, use ClickHouse's JSON functions:

```sql
SELECT
  JSONExtractString(properties, 'plan') AS plan,
  count()
FROM events
WHERE project_id = '...'
  AND event_name = 'signup'
  AND timestamp > now() - INTERVAL 30 DAY
GROUP BY plan
ORDER BY count() DESC;
```

For numeric extraction: `JSONExtractInt`, `JSONExtractFloat`. For booleans: `JSONExtractBool`.

Right now [`/v1/query`](/api-reference/query.md) returns `properties` as a JSON string for the client to parse. [`/v1/stats`](/api-reference/stats.md) doesn't yet support `group_by` on a property key — that's on the roadmap. For now, run targeted SQL via your ClickHouse credentials if you need it.

## Examples by event class

### Page view

```ts
page("/pricing", {
  variant: "v2",
  is_logged_in: false
});
```

### Conversion

```ts
track("trial_started", {
  plan: "pro",
  trial_days: 14,
  came_from: "pricing_hero"
});
```

### Engagement

```ts
track("doc_searched", {
  query_length: 12,
  results_count: 5,
  selected_index: 0
});
```

### Error

```ts
track("payment_failed", {
  error_code: "card_declined",
  attempt: 2,
  amount_cents: 4900,
  currency: "usd"
});
```

### Agent step (MCP)

```json
{
  "name": "track_event",
  "arguments": {
    "event": "tool_called",
    "anonymous_id": "agent_001",
    "user_id": "user_42",
    "properties": {
      "tool": "search_repo",
      "input_tokens": 412,
      "output_tokens": 87,
      "latency_ms": 1240,
      "model": "claude-4.6-sonnet"
    }
  }
}
```

## See also

* [Events](/core-concepts/events.md) — the surrounding event shape.
* [Event & property naming](/reference/event-naming.md) — conventions in one page.
* [Privacy](/core-concepts/privacy.md) — what we *never* store, and why.
* [POST /v1/track](/api-reference/track.md) — the endpoint.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.millimetric.ai/core-concepts/properties.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
