GraphJSON is schema-flexible, not schema-free. The API accepts changing JSON, but trustworthy metrics still depend on deliberate naming and types.
Name actions in the past tense#
Use event names that describe something that happened:
account_created
trial_started
report_exported
subscription_cancelled
Past-tense facts remain meaningful. Names such as click, process_user, or new_event are ambiguous once they reach a dashboard.
Keep event names lowercase and use one separator convention. Do not alternate among Signup, sign_up, and signed-up.
Separate identity from properties#
Include durable identifiers:
{
"event": "report_exported",
"user_id": "usr_42",
"account_id": "acct_7",
"report_id": "rpt_93",
"format": "csv"
}
Use your database identifiers, not mutable email addresses or display names. Add readable properties only when they are useful for analysis.
For multi-tenant products, include both user_id and account_id. User-level retention and account-level revenue answer different questions.
Keep quantities numeric#
Send:
{
"amount": 4900,
"currency": "usd",
"duration_ms": 283,
"row_count": 842
}
Document the unit in the field name when it is not obvious. Integer minor units such as cents avoid floating-point ambiguity for money.
Do not send:
{
"amount": "$49.00",
"duration": "283ms",
"row_count": "842 rows"
}
Formatted strings are presentation, not analytics data.
Prefer bounded dimensions#
Fields used for splits should have a small, meaningful set of values:
{
"plan": "pro",
"source": "organic",
"device_type": "mobile"
}
High-cardinality values such as request_id, a full URL with arbitrary query parameters, or raw error messages are useful for inspection but poor chart splits.
Normalize them into bounded dimensions:
{
"route": "/api/reports/:id",
"status_code": 500,
"error_type": "database_timeout",
"request_id": "req_..."
}
Model state changes as facts#
When a business object changes, log the change:
{
"event": "subscription_status_changed",
"subscription_id": "sub_12",
"account_id": "acct_7",
"previous_status": "trialing",
"status": "active"
}
If the same object can emit multiple updates, use SQL deduplication when you need its latest state. See Deduplicate events.
Version breaking changes#
Add a version when changing meaning or type:
{
"event": "checkout_completed",
"schema_version": 2,
"amount": 4900,
"currency": "usd"
}
A version is warranted when:
- a field changes type
- a unit changes
- the definition of an event changes
- one event is split into multiple events
Additive optional fields usually do not require a new version.
Protect privacy#
Only log data you need for a defined analysis or operational purpose. Prefer internal IDs over names and email addresses. Never send passwords, authentication tokens, payment card data, or secrets.
Choose collection retention based on the sensitivity and useful lifetime of the data, not a default assumption that everything should live forever.
Review with a metric#
Before shipping an event, write one query or chart it should support. If you cannot express the intended metric from the proposed fields, the event contract is incomplete.
A useful review asks:
- What exactly happened?
- Who or what did it happen to?
- When did it happen?
- Which properties explain the outcome?
- What are the units and allowed values?
- Which fields are sensitive?
- Who owns future changes?
Turn these decisions into an owned, testable contract with the tracking plan and instrumentation QA guide. Use Data lifecycle and privacy before adding direct identifiers, free text, or raw request context.