Understanding Analytics and App Events in MakerKit

Learn how the Analytics and App Events systems work together to provide centralized, maintainable event tracking in your MakerKit SaaS application.

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:

// 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:

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:

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

EventWhen EmittedAnalytics Action
user.signedInAfter successful loginidentify(userId)
user.signedUpAfter registrationtrackEvent('user.signedUp')
user.updatedAfter profile updatetrackEvent('user.updated')
checkout.startedWhen billing checkout beginstrackEvent('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:

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:

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:

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:

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:

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:

// 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

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

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

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:

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

Frequently Asked Questions

Can I use analytics without App Events?
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.
How do I track events on the server side?
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.
Are page views tracked automatically?
Yes. The AnalyticsProvider component tracks page views whenever the Next.js route changes. You only need manual tracking for virtual page views in SPAs.
How do I debug which events are firing?
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.
Can I emit events from Server Components?
No. App Events use React Context which requires a client component. Emit events from client components or use the server-side analytics API directly.
What happens if no analytics provider is configured?
Events dispatch to the NullAnalyticsService which silently ignores them. Your application continues to work without errors.

Next Steps