When someone asks why signup conversion dropped, a generic button_click event rarely answers the question. The team needs to agree on where the journey starts, what counts as completion, how users are counted, and how failures and retries are represented.
An event tracking plan records those agreements before instrumentation. It connects a business question to event names, trigger conditions, identity rules, properties and acceptance tests. A working collector cannot resolve a disagreement about what an event means.
This guide uses a fictional SaaS signup flow. The examples are teaching fixtures, not customer results. I work on SensorFlow, a self-hosted event analytics project; the design method below applies independently of the analytics tool you choose. AI assisted the preparation of this article, and its references and product claims were checked before publication.
Start with a question you can calculate
Instead of asking for "signup conversion," write a more precise question:
Of the distinct visitors who opened the signup page during a calendar day in our reporting timezone, how many completed account creation within 24 hours of their first qualifying page visit?
This defines the starting population, completion condition, identity unit, reporting timezone and conversion window. You still need to define how an anonymous visitor maps to an account, and whether repeated visits restart the window. Those decisions belong in the metric definition.
Amplitude's event selection guide works back from the questions a product team wants to answer. Capturing more interactions does not automatically produce a better measurement plan. Autocapture can help explore interface behavior, but a click or form submission is not proof that an account was created. PostHog documents automatic capture and custom events separately; custom business events let you express the outcome your system can actually confirm.
Define a small event contract
For each event, write down the following fields. The table describes a suggested contract, rather than a required schema for any vendor.
| Field | Question to resolve | Signup example |
|---|---|---|
| Event name | What action or state occurred? |
signup_submitted, account_created
|
| Trigger | At what exact point is it emitted? | Emit account_created after the account transaction commits |
| Source | Which component owns that state? | Browser for intent; backend for confirmed creation |
| Identity | Which stable identifier is used? | Anonymous visitor before signup; account ID after creation |
| Time | When did it occur, and which timezone does the report use? | Preserve occurrence time and define the reporting timezone |
| Required properties | Which dimensions are essential? |
signup_method, app_version
|
| Optional properties | How is missing context represented? | Missing campaign_id is unknown, not silently "direct" |
| Retry rule | Can one operation produce multiple events? | Reuse a stable operation ID and define deduplication |
| Owner and version | Who approves a changed meaning? | Product and engineering owners, plus a contract version |
Snowplow's tracking design documentation combines event purpose, trigger conditions and data structures in event specifications. Amplitude's tracking plan documentation also records sources, descriptions and property rules. Both are useful references when turning a short product request into something an engineer can implement and a tester can verify.
Keep names stable and put context in properties
Avoid separate event names such as signup_from_ad and signup_from_email when the action is the same. A stable signup_submitted event with a signup_source property is easier to extend when another channel appears. Segment's tracking plan guidance similarly advises against dynamic event names and property keys.
Choose a naming convention and apply it consistently. Title Case or snake_case can both work; the problem is a team alternating between them without a defined mapping. Give each property a type, allowed values, and a rule for missing data. Keep email addresses, phone numbers and full form contents out of a generic analytics payload unless a separately reviewed use case requires them.
Do not merge intent and outcome just to reduce the event count. signup_submitted means a user tried. account_created means the business state changed. If both become a vague signup, a validation error can look like a successful conversion.
Some tools provide recommended events, such as those described in Google Analytics' event guide. Define your own business meaning first, then map it to each destination. Similar event names do not guarantee equivalent semantics across tools.
Instrument at the agreed trigger
For a project already using the official Sensors Data JavaScript SDK, a custom intent event could have this shape after SDK initialization:
// Record the attempt. This is not proof of account creation.
sensors.track('signup_submitted', {
signup_method: 'email',
signup_source: 'website',
contract_version: '1',
});
This is an instrumentation fragment, not a standalone application or a live ingestion test. Initialize the SDK, identity handling and destination for your own environment; SensorFlow's SDK integration guide provides project-specific setup references. Consult the official documentation for your exact SDK version.
Let the component that can confirm the final business state own the success event. An HTTP 200 response might only mean a request was accepted. If the browser and backend both emit account_created, define how they are reconciled, and where a stable operation ID is generated. A property called operation_id does not create deduplication by itself: the receiving and analytical layers must use it according to the contract.
Test the contract, then test the metric
Prepare a small set of non-sensitive fixtures with expected event sequences:
- An anonymous visitor opens the signup page and leaves. Expect the start event and no creation event.
- Form validation fails. Specify whether
signup_submittedmeans every submit attempt or only a valid request; expect no creation event in either case. - An account is created. Expect one confirmed creation with the correct property types.
- A network failure causes a retry. Check how the same operation is counted, not just how many requests were sent.
- A visitor signs in or continues on another device. Verify the documented anonymous-to-account mapping instead of assuming two tools merge identities identically.
Check raw records against the contract first. Then recalculate the metric using the same identity, timezone, window and retry rules as the report. Receiving an event and calculating an interpretable conversion rate are separate acceptance criteria.
Keep expected and actual results with the release record. Those fixtures become regression tests when the SDK, application workflow or analytics destination changes.
Choose an analytics system against the contract
An open-source or self-hosted analytics system should be evaluated against the events and questions your team needs. Check whether it receives your existing instrumentation, exposes raw records, explains identity and time behavior, and provides the analyses your users need. Also account for the deployment, access control, monitoring and upgrade work your team will own.
SensorFlow is one option for teams retaining an existing Sensors Data SDK while controlling the event data pipeline. The open-source repository supports deployment and demonstration data; real SDK ingestion requires license activation. It is a focused self-hosted path rather than a feature-equivalent replacement for a mature all-in-one analytics suite, and it has no official affiliation with Sensors Data.
Start by writing one event contract and checking these five fixtures. Once the team can explain the result by hand, it has a concrete basis for selecting an analytics system and reviewing its reports.
Top comments (1)
the intent vs outcome split is the one i'd underline. i merged them on my own analytics at one point and every failed validation looked like a successful signup for a while.
missing campaign_id being "unknown" instead of silently "direct" is a good call too. direct already gets credit for everything nobody can explain.
the operation_id line is the trap nobody talks about though. everyone adds the property and calls it deduplication, while the receiving layer ignores it completely.