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, notbutton_click. - Be descriptive but concise:
checkout_completed, notuser_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 updatesnavigation— page views and navigationconversion— goals and revenue eventsengagement— feature usage and interactionserror— error trackingperformance— 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;
