Skip to content
trakoo
Esc
navigateopen⌘Jpreview
On this page

Events

Define type-safe analytics events once and track them across every provider.

Events are the core of analytics tracking. Each one represents an action in your app — a button click, a signup, a purchase — that you want to measure. In trakoo you define events once, in a typed collection, and every track() call is checked against those definitions.

Defining events

Describe your events in a single collection. The object keys organize your code; the name values are what trakoo sends to providers.

import type { CreateEventDefinition, EventCollection } from 'trakoo';

export const appEvents = {
  userSignedUp: {
    name: 'user_signed_up',
    category: 'user',
    properties: {} as {
      email: string;
      plan: 'free' | 'pro' | 'enterprise';
      referralSource?: string;
    }
  },
  buttonClicked: {
    name: 'button_clicked',
    category: 'engagement',
    properties: {} as {
      buttonId: string;
      location: string;
    }
  }
} as const satisfies EventCollection<Record<string, CreateEventDefinition<string>>>;

The as const satisfies pattern is what gives you exact event names, autocomplete on properties, and a compile error when a definition or a track() call is wrong. See Type Safety for how it works.

The three parts

Every event definition has a name, an optional category, and a set of properties.

Name

The name is the string sent to your providers. A few conventions keep your data clean:

  • Use snake_case — it matches most analytics tools.
  • Use past tense: button_clicked, not button_click.
  • Be descriptive but concise: checkout_completed, not user_clicked_complete_checkout_button.

Once an event is live, treat its name as stable. Renaming it splits your history — add a new event and deprecate the old one instead.

Category

Categories group related events. The field is optional, but it keeps large collections navigable. trakoo ships a set of common categories:

  • user — lifecycle events like signup, login, and profile updates
  • navigation — page views and navigation
  • conversion — goals and revenue events
  • engagement — feature usage and interactions
  • error — error tracking
  • performance — performance monitoring

You can also use your own domain-specific categories such as product, billing, or ai:

export const appEvents = {
  aiResponseGenerated: {
    name: 'ai_response_generated',
    category: 'ai',
    properties: {} as {
      model: string;
      tokensUsed: number;
      responseTime: number;
    }
  }
} as const;

Properties

Properties are the data attached to each event. Type them as narrowly as the data allows — specific types catch mistakes at the call site and keep your reports consistent.

// Good — narrow types and explicit units
properties: {} as {
  plan: 'free' | 'pro' | 'enterprise';
  amount: number;
  currency: 'USD' | 'EUR' | 'GBP';
}

// Avoid — anything goes, nothing is checked
properties: {} as {
  plan: string;
  amount: any;
  currency: string;
}

Mark optional properties with ?.

Tracking events

With events defined, track() takes the event key and its typed properties. Mistype the name or omit a required property, and TypeScript flags it before the event ships.

Client analytics is fire-and-forget, so calls don’t block the UI:

import { analytics } from '@/lib/analytics';

analytics.track('button_clicked', {
  buttonId: 'cta',
  location: 'hero'
});

Server analytics is stateless: pass user context with each call, and flush before the process exits.

import { serverAnalytics } from '@/lib/server-analytics';

await serverAnalytics.track('user_signed_up', {
  email: 'ada@example.com',
  plan: 'pro'
}, {
  userId: 'user_123',
  user: { email: 'ada@example.com' }
});

await serverAnalytics.shutdown();

See Client vs Server for where each kind of event belongs.

Organizing events

For a small app, one file is enough:

export const appEvents = {
  userSignedUp: { /* ... */ },
  productViewed: { /* ... */ },
  buttonClicked: { /* ... */ }
} as const;

As the collection grows, split by area and merge in an index:

import { userEvents } from './user';
import { productEvents } from './product';

export const appEvents = {
  ...userEvents,
  ...productEvents
} as const;

Common patterns

E-commerce apps track the path from view to purchase:

export const appEvents = {
  productViewed: {
    name: 'product_viewed',
    category: 'product',
    properties: {} as {
      productId: string;
      price: number;
    }
  },
  purchaseCompleted: {
    name: 'purchase_completed',
    category: 'conversion',
    properties: {} as {
      orderId: string;
      total: number;
      currency: 'USD' | 'EUR' | 'GBP';
    }
  }
} as const;

SaaS apps focus on activation and upgrades:

export const appEvents = {
  trialStarted: {
    name: 'trial_started',
    category: 'conversion',
    properties: {} as {
      plan: 'pro' | 'enterprise';
      trialDays: number;
    }
  },
  subscriptionUpgraded: {
    name: 'subscription_upgraded',
    category: 'conversion',
    properties: {} as {
      fromPlan: string;
      toPlan: string;
      price: number;
    }
  }
} as const;

Best practices

  • Define events before you track them. A central, typed collection means autocomplete works everywhere and typos become compile errors.
  • Document non-obvious events with a comment on when and why they fire, and who relies on them.
  • Keep names stable in production. Add and deprecate rather than rename.
export const appEvents = {
  // @deprecated Use 'subscription_upgraded' instead
  planUpgraded: { /* ... */ },
  subscriptionUpgraded: { /* ... */ }
} as const;

Next steps

Was this page helpful?