Next.js Supabase V3 (#463)
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
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
363
docs/analytics/analytics-and-events.mdoc
Normal file
363
docs/analytics/analytics-and-events.mdoc
Normal file
@@ -0,0 +1,363 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user