Concepts
Tracking on successful states
Track outcomes, not intent. The rule behind most analytics bugs.
There's a deceptively important rule in analytics implementation: track outcomes, not intent. Most analytics bugs trace back to ignoring this rule.
The rule
Don't fire an event because the user clicked a button. Fire it because the action the button initiated actually succeeded.
Wrong:
<button onClick={() => {
amplitude.track("Invoice created");
submitInvoice();
}}>
Create invoice
</button>Right:
const handleSubmit = async () => {
const result = await submitInvoice();
if (result.success) {
amplitude.track("Invoice created");
}
};The first version fires the event whether or not the invoice was actually created. Validation fails? Event fires. Network drops? Event fires. Server throws 500? Event fires. Your "Invoice created" count is now inflated by failures.
The second version fires the event only when the server has confirmed the invoice exists. Your analytics now reflect reality.
Why this matters
If you track attempts instead of outcomes, three things go wrong.
Conversion rates are inflated. Your "users who created an invoice" cohort includes users who tried to and failed. Cohorts you build on top of this — "users who created and then sent an invoice" — start to look better than they are.
Failure modes hide. If 30% of "Invoice created" events are actually failures, you don't see that as a 30% failure rate. You see it as "Invoice created" growing, even though a third of those users had a broken experience.
You can't trust the data. Once you find one bug like this, you wonder which other events are tracked on intent. The trust erodes across the board, and people stop using the dashboards.
The pattern
For any event that depends on state changing in your database (an invoice being created, a customer being added, a subscription starting):
- The user takes an action
- Your frontend sends the request to your backend
- Your backend processes it, returns success or failure
- Then the analytics event fires — on the success branch only
You'll either fire the event on the client after a successful response, or fire it on the server after the database write completes. Both work; the choice is a client vs. server decision.
If you fire on the client:
const result = await createInvoice(data);
if (result.success) {
amplitude.track("Invoice created", { /* properties */ });
}If you fire on the server:
const invoice = await db.invoices.create(data);
await track("Invoice created", { /* properties */ }, {
user_id, device_id, session_id
});When this rule doesn't apply
Some events don't represent state changes. They represent immediate UI feedback — opening a dropdown, expanding an accordion, dismissing a banner. There's no server to wait on, because the action's outcome is the UI itself.
These are micro events, covered in the next chapter. They fire immediately, without server confirmation, because the UI state is the source of truth.
The rule above applies to heavy events — events that represent state changes in your database. The distinction is so important it has its own chapter.
Edge cases
Optimistic UIs. If your app updates the UI before the server confirms (showing the invoice as created while the request is still in flight), should the event fire optimistically too?
No. Fire on server confirmation. If the request fails and the optimistic update reverts, the analytics event would otherwise be a lie. Better to delay the event a few hundred milliseconds than to have data that doesn't match reality.
Background syncs. If your app saves changes in the background as the user types, when does the event fire? Pick the moment that matters analytically. For a draft system, probably not on every keystroke — maybe on first save, or on user-initiated save.
Long-running operations. If something takes 30 seconds to complete (large file upload, background job), the user might navigate away before the success event fires. Server-side tracking is the only reliable answer here — the event fires when the operation completes server-side, regardless of where the user is.
What to take away
If the event represents something happening in your database, wait for confirmation. Always. The 200ms extra latency between click and event fire is invisible to the user; the data quality difference is enormous.