Properties
What to put in `properties`, what the SDK adds automatically, and how to design a clean schema.
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.
{
"event": "purchase",
"properties": {
"amount_cents": 4900,
"currency": "usd",
"items": 2,
"plan": "pro",
"discount_code": "SPRING25"
}
}Limits:
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.
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
propertiesas aString; the API parses your object once on insert and re-serialises it. Sendingproperties: "{\"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 merges these into properties:
Plus on the event itself (not in properties):
The classifier reads url + referrer to produce source / medium / campaign / source_confidence / source_rule_id server-side — see Attribution.
You can override anything by passing it explicitly:
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:
countryfrom the request IP via Cloudflare geo headers.device_type / browser / osfrom the User-Agent header — though for backend events that's normally "the request that triggered this server-sidetrack", which may not match the visitor's actual browser. Passurl+referrerfrom 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.
Property semantics that downstream tools rely on
Some properties are special because the API or MCP tools index on them.
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:
For numeric extraction: JSONExtractInt, JSONExtractFloat. For booleans: JSONExtractBool.
Right now /v1/query returns properties as a JSON string for the client to parse. /v1/stats 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
Conversion
Engagement
Error
Agent step (MCP)
See also
Events — the surrounding event shape.
Event & property naming — conventions in one page.
Privacy — what we never store, and why.
POST /v1/track — the endpoint.
Last updated
Was this helpful?