Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
364 lines
12 KiB
Plaintext
364 lines
12 KiB
Plaintext
---
|
|
status: "published"
|
|
title: 'Understanding Analytics and App Events in MakerKit'
|
|
label: 'Analytics and Events'
|
|
description: 'Learn how the Analytics and App Events systems work together to provide centralized, maintainable event tracking in your MakerKit SaaS application.'
|
|
order: 0
|
|
---
|
|
|
|
MakerKit separates event emission from analytics tracking through two interconnected systems: **App Events** for broadcasting important occurrences in your app, and **Analytics** for tracking user behavior. This separation keeps your components clean and your analytics logic centralized.
|
|
|
|
## Why Centralized Analytics
|
|
|
|
Scattering `analytics.trackEvent()` calls throughout your codebase creates maintenance problems. When you need to change event names, add properties, or switch providers, you hunt through dozens of files.
|
|
|
|
The centralized approach solves this:
|
|
|
|
```typescript
|
|
// Instead of this (scattered analytics)
|
|
function CheckoutButton() {
|
|
const handleClick = () => {
|
|
analytics.trackEvent('checkout_started', { plan: 'pro' });
|
|
analytics.identify(userId);
|
|
mixpanel.track('Checkout Started');
|
|
// More provider-specific code...
|
|
};
|
|
}
|
|
|
|
// Do this (centralized via App Events)
|
|
function CheckoutButton() {
|
|
const { emit } = useAppEvents();
|
|
|
|
const handleClick = () => {
|
|
emit({ type: 'checkout.started', payload: { planId: 'pro' } });
|
|
};
|
|
}
|
|
```
|
|
|
|
The analytics mapping lives in one place: `apps/web/components/analytics-provider.tsx`.
|
|
|
|
## How It Works
|
|
|
|
The system has three parts:
|
|
|
|
1. **App Events Provider**: A React Context that provides `emit`, `on`, and `off` functions
|
|
2. **Analytics Provider**: Subscribes to App Events and maps them to analytics calls
|
|
3. **Analytics Manager**: Dispatches events to all registered analytics providers
|
|
|
|
```
|
|
Component Analytics Provider Providers
|
|
│ │ │
|
|
│ emit('checkout.started') │ │
|
|
│────────────────────────────▶│ │
|
|
│ │ analytics.trackEvent │
|
|
│ │────────────────────────▶│
|
|
│ │ analytics.identify │
|
|
│ │────────────────────────▶│
|
|
```
|
|
|
|
## Emitting Events
|
|
|
|
Use the `useAppEvents` hook to emit events from any component:
|
|
|
|
```typescript
|
|
import { useAppEvents } from '@kit/shared/events';
|
|
|
|
function FeatureButton() {
|
|
const { emit } = useAppEvents();
|
|
|
|
const handleClick = () => {
|
|
emit({
|
|
type: 'feature.used',
|
|
payload: { featureName: 'export' }
|
|
});
|
|
};
|
|
|
|
return <button onClick={handleClick}>Export</button>;
|
|
}
|
|
```
|
|
|
|
The event is broadcast to all listeners, including the analytics provider.
|
|
|
|
## Default Event Types
|
|
|
|
MakerKit defines these base event types in `@kit/shared/events`:
|
|
|
|
```typescript
|
|
interface BaseAppEventTypes {
|
|
'user.signedIn': { userId: string };
|
|
'user.signedUp': { method: 'magiclink' | 'password' };
|
|
'user.updated': Record<string, never>;
|
|
'checkout.started': { planId: string; account?: string };
|
|
}
|
|
```
|
|
|
|
These events are emitted automatically by MakerKit components and mapped to analytics calls.
|
|
|
|
### Event Descriptions
|
|
|
|
| Event | When Emitted | Analytics Action |
|
|
|-------|--------------|------------------|
|
|
| `user.signedIn` | After successful login | `identify(userId)` |
|
|
| `user.signedUp` | After registration | `trackEvent('user.signedUp')` |
|
|
| `user.updated` | After profile update | `trackEvent('user.updated')` |
|
|
| `checkout.started` | When billing checkout begins | `trackEvent('checkout.started')` |
|
|
|
|
**Note**: The `user.signedUp` event does not fire automatically for social/OAuth signups. You may need to emit it manually in your OAuth callback handler.
|
|
|
|
## Creating Custom Events
|
|
|
|
Define custom events by extending `ConsumerProvidedEventTypes`:
|
|
|
|
```typescript {% title="lib/events/custom-events.ts" %}
|
|
import { ConsumerProvidedEventTypes } from '@kit/shared/events';
|
|
|
|
export interface MyAppEvents extends ConsumerProvidedEventTypes {
|
|
'feature.used': { featureName: string; duration?: number };
|
|
'project.created': { projectId: string; template: string };
|
|
'export.completed': { format: 'csv' | 'json' | 'pdf'; rowCount: number };
|
|
}
|
|
```
|
|
|
|
Use the typed hook in your components:
|
|
|
|
```typescript
|
|
import { useAppEvents } from '@kit/shared/events';
|
|
import type { MyAppEvents } from '~/lib/events/custom-events';
|
|
|
|
function ProjectForm() {
|
|
const { emit } = useAppEvents<MyAppEvents>();
|
|
|
|
const handleCreate = (project: Project) => {
|
|
emit({
|
|
type: 'project.created',
|
|
payload: {
|
|
projectId: project.id,
|
|
template: project.template,
|
|
},
|
|
});
|
|
};
|
|
}
|
|
```
|
|
|
|
TypeScript enforces the correct payload shape for each event type.
|
|
|
|
## Mapping Events to Analytics
|
|
|
|
The `AnalyticsProvider` component maps events to analytics calls. Add your custom events here:
|
|
|
|
```typescript {% title="apps/web/components/analytics-provider.tsx" %}
|
|
const analyticsMapping: AnalyticsMapping = {
|
|
'user.signedIn': (event) => {
|
|
const { userId, ...traits } = event.payload;
|
|
if (userId) {
|
|
return analytics.identify(userId, traits);
|
|
}
|
|
},
|
|
'user.signedUp': (event) => {
|
|
return analytics.trackEvent(event.type, event.payload);
|
|
},
|
|
'checkout.started': (event) => {
|
|
return analytics.trackEvent(event.type, event.payload);
|
|
},
|
|
// Add custom event mappings
|
|
'feature.used': (event) => {
|
|
return analytics.trackEvent('Feature Used', {
|
|
feature_name: event.payload.featureName,
|
|
duration: String(event.payload.duration ?? 0),
|
|
});
|
|
},
|
|
'project.created': (event) => {
|
|
return analytics.trackEvent('Project Created', {
|
|
project_id: event.payload.projectId,
|
|
template: event.payload.template,
|
|
});
|
|
},
|
|
};
|
|
```
|
|
|
|
This is the only place you need to modify when changing analytics behavior.
|
|
|
|
## Listening to Events
|
|
|
|
Beyond analytics, you can subscribe to events for other purposes:
|
|
|
|
```typescript
|
|
import { useAppEvents } from '@kit/shared/events';
|
|
import { useEffect } from 'react';
|
|
|
|
function NotificationListener() {
|
|
const { on, off } = useAppEvents();
|
|
|
|
useEffect(() => {
|
|
const handler = (event) => {
|
|
showToast(`Project ${event.payload.projectId} created!`);
|
|
};
|
|
|
|
on('project.created', handler);
|
|
return () => off('project.created', handler);
|
|
}, [on, off]);
|
|
|
|
return null;
|
|
}
|
|
```
|
|
|
|
This pattern is useful for triggering side effects like notifications, confetti animations, or feature tours.
|
|
|
|
## Direct Analytics API
|
|
|
|
While centralized events are recommended, you can use the analytics API directly when needed:
|
|
|
|
```typescript
|
|
import { analytics } from '@kit/analytics';
|
|
|
|
// Identify a user
|
|
void analytics.identify('user_123', {
|
|
email: 'user@example.com',
|
|
plan: 'pro',
|
|
});
|
|
|
|
// Track an event
|
|
void analytics.trackEvent('Button Clicked', {
|
|
button: 'submit',
|
|
page: 'settings',
|
|
});
|
|
|
|
// Track a page view (usually automatic)
|
|
void analytics.trackPageView('/dashboard');
|
|
```
|
|
|
|
Use direct calls for one-off tracking that does not warrant an event type.
|
|
|
|
## Automatic Page View Tracking
|
|
|
|
The `AnalyticsProvider` automatically tracks page views when the Next.js route changes:
|
|
|
|
```typescript
|
|
// This happens automatically in AnalyticsProvider
|
|
function useReportPageView(reportFn: (url: string) => unknown) {
|
|
const pathname = usePathname();
|
|
|
|
useEffect(() => {
|
|
const url = pathname;
|
|
reportFn(url);
|
|
}, [pathname]);
|
|
}
|
|
```
|
|
|
|
You do not need to manually track page views unless you have a specific use case.
|
|
|
|
## Common Patterns
|
|
|
|
### Track Form Submissions
|
|
|
|
```typescript
|
|
function ContactForm() {
|
|
const { emit } = useAppEvents<MyAppEvents>();
|
|
|
|
const handleSubmit = async (data: FormData) => {
|
|
await submitForm(data);
|
|
|
|
emit({
|
|
type: 'form.submitted',
|
|
payload: { formName: 'contact', fields: Object.keys(data).length },
|
|
});
|
|
};
|
|
}
|
|
```
|
|
|
|
### Track Feature Engagement
|
|
|
|
```typescript
|
|
function AIAssistant() {
|
|
const { emit } = useAppEvents<MyAppEvents>();
|
|
const startTime = useRef<number>();
|
|
|
|
const handleOpen = () => {
|
|
startTime.current = Date.now();
|
|
};
|
|
|
|
const handleClose = () => {
|
|
const duration = Date.now() - (startTime.current ?? Date.now());
|
|
|
|
emit({
|
|
type: 'feature.used',
|
|
payload: { featureName: 'ai-assistant', duration },
|
|
});
|
|
};
|
|
}
|
|
```
|
|
|
|
### Track Errors
|
|
|
|
```typescript
|
|
function ErrorBoundary({ children }) {
|
|
const { emit } = useAppEvents<MyAppEvents>();
|
|
|
|
const handleError = (error: Error) => {
|
|
emit({
|
|
type: 'error.occurred',
|
|
payload: {
|
|
message: error.message,
|
|
stack: error.stack?.slice(0, 500),
|
|
},
|
|
});
|
|
};
|
|
}
|
|
```
|
|
|
|
## Debugging Events
|
|
|
|
During development, add logging to your event handlers to verify events are emitting correctly:
|
|
|
|
```typescript {% title="apps/web/components/analytics-provider.tsx" %}
|
|
const analyticsMapping: AnalyticsMapping = {
|
|
'user.signedIn': (event) => {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log('[Analytics Event]', event.type, event.payload);
|
|
}
|
|
|
|
const { userId, ...traits } = event.payload;
|
|
if (userId) {
|
|
return analytics.identify(userId, traits);
|
|
}
|
|
},
|
|
'checkout.started': (event) => {
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.log('[Analytics Event]', event.type, event.payload);
|
|
}
|
|
|
|
return analytics.trackEvent(event.type, event.payload);
|
|
},
|
|
// Add logging to other handlers as needed
|
|
};
|
|
```
|
|
|
|
You can also use your analytics provider's debug mode (PostHog and GA4 both offer live event views in their dashboards).
|
|
|
|
## Best Practices
|
|
|
|
1. **Use App Events for domain events**: Business-relevant events (signup, purchase, feature use) should go through App Events
|
|
2. **Keep payloads minimal**: Only include data you will actually analyze
|
|
3. **Use consistent naming**: Follow a pattern like `noun.verb` (user.signedUp, project.created)
|
|
4. **Type your events**: Define interfaces for compile-time safety
|
|
5. **Test event emission**: Verify critical events emit during integration tests
|
|
6. **Document your events**: Maintain a list of events and their purposes
|
|
|
|
{% faq
|
|
title="Frequently Asked Questions"
|
|
items=[
|
|
{"question": "Can I use analytics without App Events?", "answer": "Yes. Import analytics from @kit/analytics and call trackEvent directly. However, the centralized approach through App Events is easier to maintain as your application grows."},
|
|
{"question": "How do I track events on the server side?", "answer": "Import analytics from @kit/analytics/server. Note that only PostHog supports server-side analytics out of the box. The App Events system is client-side only."},
|
|
{"question": "Are page views tracked automatically?", "answer": "Yes. The AnalyticsProvider component tracks page views whenever the Next.js route changes. You only need manual tracking for virtual page views in SPAs."},
|
|
{"question": "How do I debug which events are firing?", "answer": "Add a wildcard handler in your analytics mapping that logs events in development mode. You can also use browser DevTools or your analytics provider's debug mode."},
|
|
{"question": "Can I emit events from Server Components?", "answer": "No. App Events use React Context which requires a client component. Emit events from client components or use the server-side analytics API directly."},
|
|
{"question": "What happens if no analytics provider is configured?", "answer": "Events dispatch to the NullAnalyticsService which silently ignores them. Your application continues to work without errors."}
|
|
]
|
|
/%}
|
|
|
|
## Next Steps
|
|
|
|
- [Set up Google Analytics](google-analytics-provider) for marketing analytics
|
|
- [Set up PostHog](posthog-analytics-provider) for product analytics with feature flags
|
|
- [Create a custom provider](custom-analytics-provider) to integrate other services
|