---
title: Feature tracking
description: A template for instrumenting feature usage. Apply once per major area of your product.
readingTime: 22 min
---

# Feature tracking

> **Chapter 3 of the Amplitude implementation section.** Assumes you've completed [Authentication tracking](./authentication.md) and read [Heavy events vs. micro events](../concepts/heavy-vs-micro-events.md).

Feature tracking isn't a single module — it's a template you apply to each major area of your product. This chapter covers the patterns; you'll repeat them for every feature you instrument.

The recommendation here: track most feature events client-side. Users are actively in your app when these fire, and the small loss to ad blockers rarely changes how you'd interpret the data. For events that absolutely need accuracy (revenue), server-side is the answer — but that's the next chapter, not this one.

## Planning the events for a feature

Before writing code, map out the feature's user journey and identify the meaningful actions. Most useful events represent **state transitions** — moments when data is created, modified, or deleted.

Worked example: an invoicing feature. The user flow:

1. User creates a new invoice (blank, or from a template)
2. User adds line items, sets payment terms
3. User sends the invoice to a client
4. User may later edit, duplicate, or void the invoice

From this, the events worth tracking:

- `Invoice created`
- `Invoice edited`
- `Invoice sent`
- `Invoice voided`
- `Template created`
- `Template used`

Each fires only when the corresponding action actually succeeds (see [Tracking on successful states](../concepts/tracking-on-successful-states.md)). If the user clicks "Send" but the email fails, no `Invoice sent` event.

You may also want a few micro events to measure drop-off within flows (see [Heavy events vs. micro events](../concepts/heavy-vs-micro-events.md)):

- `Customer dropdown opened`
- `Customer selected`
- `Invoice line item added`

These fire on UI state changes, without server confirmation.

## Picking properties

Properties answer questions about *how* a feature is used. For invoice creation, useful properties include:

- `from_template: true | false`
- `line_item_count: 5`
- `total_amount: 1500`
- `currency: "EUR"`
- `template_id: "abc123"` (if from_template is true)

Properties should answer business questions. They should *not* include PII — no customer names, no email addresses, no invoice IDs that could identify a specific person. These don't help understand product usage and create compliance risk.

A good test: would this property help you answer "are users with X behavior more likely to retain?" If yes, include it. If it's just descriptive metadata for a specific record, leave it out.

## Heavy events (state changes)

Heavy events wait for server confirmation. The pattern:

```tsx
"use client";

import { createInvoice } from "./actions";
import amplitude from "@/components/amplitude-client";

export function CreateInvoiceForm({ customers }: { customers: Customer[] }) {
  async function handleSubmit(formData: FormData) {
    const result = await createInvoice(formData);
    
    if (result.success) {
      amplitude.track("Invoice created", {
        from_template: formData.get("template_id") ? true : false,
        line_item_count: formData.getAll("line_items").length,
        total_amount: result.invoice.totalAmount,
        currency: result.invoice.currency,
      });
    } else {
      amplitude.track("Error occurred", {
        feature: "invoice_creation",
        error_type: result.error,
      });
    }
  }
  
  return (
    <form action={handleSubmit}>
      {/* form fields */}
    </form>
  );
}
```

The event fires only after `result.success` confirms the invoice was actually created. If validation fails or the network drops, the success event doesn't fire — but the error event does, with enough context to identify what failed.

### Why include an error event?

Tracking errors as a separate event lets you measure how often features fail for users. This isn't operational debugging (that's what your error logger is for) — it's understanding how often users hit a wall in your product.

If 5% of "attempted invoice creation" requests result in an error event, that's actionable. You can find the most common error types, fix the worst offenders, and measure the impact.

You don't need a separate event per failure type. One `Error occurred` event with a `feature` property and an `error_type` property keeps your taxonomy clean while preserving the granularity to filter when needed.

### When *not* to track errors

Only track errors that affect the user-facing experience. Skip:

- Network timeouts that retry transparently
- Validation errors before the request is sent (these don't reach your server)
- Backend errors that don't surface to the user (background job failures, async retries)

These belong in your error logger, not your analytics. The bar for tracking an error in Amplitude is: "did the user actually see this fail?"

## Micro events (UI state changes)

Micro events fire on UI state changes, immediately, without server confirmation.

```tsx
"use client";

import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import amplitude from "@/components/amplitude-client";

export function CustomerSelector({ customers }: { customers: Customer[] }) {
  return (
    <Select
      name="customer"
      onOpenChange={(open) => {
        amplitude.track(open ? "Customer dropdown opened" : "Customer dropdown closed");
      }}
      onValueChange={() => {
        amplitude.track("Customer selected");
      }}
    >
      <SelectTrigger>
        <SelectValue placeholder="Select a customer" />
      </SelectTrigger>
      <SelectContent>
        {customers.map((customer) => (
          <SelectItem key={customer.id} value={customer.id}>{customer.name}</SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}
```

These events power funnel analyses. When you're trying to understand why users abandon the invoice creation flow, the funnel from "Customer dropdown opened" → "Customer selected" → "Invoice created" tells you where they stop.

### Don't over-track micro events

It's easy to instrument every UI interaction once you've got the pattern. Resist this. The rule of thumb: track micro events when you have a specific question about drop-off in a known flow, or when you suspect a UI element isn't being noticed.

If you're tracking 15 micro events on a single screen, you've over-instrumented. The signal gets buried in the noise. Pick the 2–3 that matter for the questions you're actually trying to answer.

## Server-side feature tracking

If your team prefers tracking from the server, the pattern matches the auth module. Pass device ID and session ID through the form submission so the event stitches into the user's session:

```ts
import { track } from '@amplitude/analytics-node';

export async function createInvoice(formData: FormData) {
  const userId = await getCurrentUserId();
  const deviceId = formData.get("amplitudeDeviceId") as string;
  const sessionId = formData.get("amplitudeSessionId") as string;
  
  try {
    const invoice = await db.invoices.create({
      // ...
    });
    
    await track("Invoice created", {
      from_template: formData.get("template_id") ? true : false,
      line_item_count: formData.getAll("line_items").length,
      total_amount: invoice.totalAmount,
      currency: invoice.currency,
    }, {
      user_id: userId,
      device_id: deviceId,
      session_id: sessionId ? parseInt(sessionId, 10) : undefined,
    });
    
    return { success: true, invoice };
  } catch (error) {
    if (error.type === 'VALIDATION_ERROR') {
      await track("Error occurred", {
        feature: "invoice_creation",
        error_type: "validation_error",
      }, {
        user_id: userId,
        device_id: deviceId,
        session_id: sessionId ? parseInt(sessionId, 10) : undefined,
      });
      return { success: false, error: error.message };
    }
    
    // Operational error — log it, don't track it
    logger.error(error, { context: 'invoice_creation', userId });
    return { success: false, error: 'Something went wrong' };
  }
}
```

Server-side gives you guaranteed delivery and ties tracking to your database transaction. It's more code, but for some events it's the right choice — particularly if your team prefers keeping data operations in one place.

For most feature events, client-side is sufficient. Save server-side for the events that absolutely need accuracy.

## Build incrementally

Once you've instrumented one feature, move to the next. Don't try to track everything at once — you'll end up with a sprawling tracking plan you can't validate.

Pick features by impact:

- Which features are critical to your activation flow?
- Which features do paying users use heavily?
- Which features have you been guessing about?

Track one. Validate it. Build a report. Then move on.

## Validation

For each feature you instrument, verify:

**Events fire correctly.**

- Heavy events fire only after the server confirms success, not on click
- Micro events fire on UI state changes
- Error events fire when actions fail
- Event names match your taxonomy exactly

**Properties are accurate.**

- All listed properties appear on every event
- Values match expected types (numbers as numbers, not strings)
- No PII or sensitive data accidentally included

**Cross-browser works.**

- Events fire in incognito/private browsing
- Ad blockers don't break the page (events may be lost, but the app should work)
- Mobile browsers behave the same as desktop

**Real user data looks reasonable.**

- Pull a few users' timelines and walk through them — does the event sequence match what they'd have actually done?
- Check property distributions — are there impossible values, blank strings where data should exist?
