Design a product analytics event naming standard
In this article (5 sections)
An event naming standard should help two people identify the same business action from the same record. Consistent spelling is useful, but the larger requirement is consistent meaning: when the event fires, what entity it describes and whether it represents an attempt or a completed outcome.
Begin with the decisions the data must support, then write a small tracking plan with executable checks.
Define the outcome before the name
For a fictional reporting product, report_publish_clicked means the interface received a click. report_published means the application confirmed a durable, successful publication. A failed request can produce the first event without the second.
The distinction matters for activation analysis. Counting button clicks as successful publication would change the value proxy without changing the dashboard label.
Use a stable object-and-action vocabulary with one casing convention. Segment's data collection guidance recommends consistent naming and keeping dynamic values in properties. The exact spelling convention is a team choice; its consistent application and documented semantics matter more than adopting one vendor's capitalization.
Write a compact event contract
For this original example, report_published version 1 requires an event ID, stable user ID, UTC occurrence timestamp, report ID and boolean success equal to true. It is emitted after confirmed publication. Retries reuse the same event ID for the same occurrence; distinct publications receive distinct IDs.
The teaching payload is separate from the simplified CSV event fixture. It illustrates richer instrumentation before those events are transformed into an analytics table.
from datetime import datetime, timezone
def validate(event):
required = {'event_id','event_name','event_version','user_id',
'occurred_at','report_id','success'}
if set(event) != required:
raise ValueError('unexpected or missing fields')
if event['event_name'] != 'report_published':
raise ValueError('unknown event')
if type(event['event_version']) is not int or event['event_version'] != 1:
raise ValueError('unsupported version')
for key in ('event_id','user_id','report_id'):
if not isinstance(event[key],str) or not event[key].strip():
raise ValueError('invalid identifier')
if type(event['success']) is not bool or not event['success']:
raise ValueError('publication was not successful')
value = event['occurred_at']
if not isinstance(value,str) or not value.endswith('Z'):
raise ValueError('UTC timestamp required')
parsed = datetime.fromisoformat(value[:-1]+'+00:00')
if parsed.tzinfo != timezone.utc:
raise ValueError('UTC timestamp required')
return event
valid = {'event_id':'E100','event_name':'report_published','event_version':1,
'user_id':'U1','occurred_at':'2026-01-15T12:00:00Z',
'report_id':'R1','success':True}
assert validate(valid) == valid
invalid = [dict(valid,success='true'), dict(valid,success=False),
dict(valid,event_version=True), dict(valid,event_version=2),
dict(valid,occurred_at='2026-01-15T12:00:00'),
dict(valid,report_id=''), dict(valid,report_title='private content')]
for payload in invalid:
try:
validate(payload)
except ValueError:
pass
else:
raise AssertionError('invalid payload accepted')
print({'accepted':1,'rejected':len(invalid)})The boolean checks are deliberate. The text 'true' is not a boolean, and Python treats booleans as integer subclasses, so a simple integer isinstance check would accept True as version 1. The example uses exact types to avoid that ambiguity.
Separate schema validity from event truth
A valid payload can still be emitted at the wrong point in the application. Test the actual workflow: successful publication emits one outcome; a failed publication emits no successful outcome; a retry preserves identity; a conflicting replay is quarantined.
The validator above checks one payload's structure and selected semantics. It does not verify storage durability, identity ownership, clock accuracy or deduplication across events. Those require integration and pipeline checks.
Record occurrence time and ingestion time separately when late arrival matters. A single timestamp cannot explain both when a user acted and when the warehouse learned about it.
Evolve contracts without splitting metrics silently
Assign an owner, downstream metrics and a change policy to each event. Adding a required property can break older clients that continue to emit the previous version. Support an explicit migration and monitor version coverage before changing metric logic.
Avoid placing report names, customer names or arbitrary text in event names. Besides fragmenting analysis, dynamic names can expose unnecessary content. Collect only properties needed for the stated analytical purpose and restrict their distribution appropriately.
Exercise: add a publication failure event with a controlled error category. Verify that failure events cannot enter the successful-publication metric and that retries do not inflate the outcome count.
NeuraPath's Data Analytics with Generative AI course connects event design with SQL and product metrics. A reliable tracking plan makes the business meaning testable before a chart is built.
Continue learning
This article is part of the Customer and product analytics sequence. Use the neighbouring tasks when you need the prerequisite or the next application.
- Review the prerequisite or neighbouring task in Analyze search behaviour using no-result queries.
- Continue with Write a decision memo from a retention analysis.
Pankit Kumar has 10 years in Data Science & AI, building and shipping production systems in regulated pharma and clinical environments. He is a freelance trainer at Boston Institute of Analytics, AnalytixLabs and Scaler, and has taught this material to thousands of working professionals.
This article is part of our Data Analytics with Generative AI programme — 3–4 months. The full analyst stack — Excel, SQL, Power BI and Python pipelines — then a generative-AI layer you can prove is right.
Explore Data Analytics with Generative AI