Creating a Custom Analytics Provider

Integrate Google Analytics, Mixpanel, PostHog, or any analytics service by implementing the AnalyticsService interface.

How to create a custom analytics provider

Add your preferred analytics service to the kit.

The analytics package is designed for extensibility. You implement the AnalyticsService interface, register your provider, and the AnalyticsManager handles dispatching events to all registered providers.

Implement the AnalyticsService Interface

Create a new file in the analytics package:

packages/analytics/src/google-analytics-service.ts

import { AnalyticsService } from './types';
export class GoogleAnalyticsService implements AnalyticsService {
private measurementId: string;
constructor(config?: { measurementId?: string }) {
this.measurementId = config?.measurementId ?? '';
}
async initialize() {
// Load the GA4 script
if (typeof window === 'undefined') return;
const script = document.createElement('script');
script.src = `https://www.googletagmanager.com/gtag/js?id=${this.measurementId}`;
script.async = true;
document.head.appendChild(script);
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() {
window.dataLayer.push(arguments);
};
window.gtag('js', new Date());
window.gtag('config', this.measurementId);
}
async identify(userId: string, traits?: Record<string, string>) {
if (typeof window === 'undefined') return;
window.gtag('config', this.measurementId, {
user_id: userId,
...traits,
});
}
async trackPageView(path: string) {
if (typeof window === 'undefined') return;
window.gtag('event', 'page_view', {
page_path: path,
});
}
async trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
) {
if (typeof window === 'undefined') return;
window.gtag('event', eventName, eventProperties);
}
}
// Type declarations for gtag
declare global {
interface Window {
dataLayer: unknown[];
gtag: (...args: unknown[]) => void;
}
}

Register the Provider

Update the analytics index file to include your provider:

packages/analytics/src/index.ts

import { createAnalyticsManager } from './analytics-manager';
import { GoogleAnalyticsService } from './google-analytics-service';
import { NullAnalyticsService } from './null-analytics-service';
import type { AnalyticsManager } from './types';
export const analytics: AnalyticsManager = createAnalyticsManager({
providers: {
google: () =>
new GoogleAnalyticsService({
measurementId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID,
}),
null: () => NullAnalyticsService,
},
});

Multiple Providers

You can register multiple providers to send events to several services simultaneously:

packages/analytics/src/index.ts

import { createAnalyticsManager } from './analytics-manager';
import { GoogleAnalyticsService } from './google-analytics-service';
import { MixpanelService } from './mixpanel-service';
import { PostHogService } from './posthog-service';
import { NullAnalyticsService } from './null-analytics-service';
import type { AnalyticsManager } from './types';
export const analytics: AnalyticsManager = createAnalyticsManager({
providers: {
google: () =>
new GoogleAnalyticsService({
measurementId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID,
}),
mixpanel: () =>
new MixpanelService({
token: process.env.NEXT_PUBLIC_MIXPANEL_TOKEN,
}),
posthog: () =>
new PostHogService({
apiKey: process.env.NEXT_PUBLIC_POSTHOG_KEY,
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
}),
null: () => NullAnalyticsService,
},
});

When you call analytics.trackEvent(), the event is sent to Google Analytics, Mixpanel, and PostHog simultaneously.

Server-Side Tracking

For server-side analytics, update the server export as well:

packages/analytics/src/server.ts

import 'server-only';
import { createAnalyticsManager } from './analytics-manager';
import { ServerAnalyticsService } from './server-analytics-service';
import { NullAnalyticsService } from './null-analytics-service';
import type { AnalyticsManager } from './types';
export const analytics: AnalyticsManager = createAnalyticsManager({
providers: {
server: () =>
new ServerAnalyticsService({
apiKey: process.env.ANALYTICS_API_KEY,
}),
null: () => NullAnalyticsService,
},
});

Server-side providers typically use HTTP APIs instead of browser scripts:

packages/analytics/src/server-analytics-service.ts

import { AnalyticsService } from './types';
export class ServerAnalyticsService implements AnalyticsService {
private apiKey: string;
private endpoint: string;
constructor(config?: { apiKey?: string; endpoint?: string }) {
this.apiKey = config?.apiKey ?? '';
this.endpoint = config?.endpoint ?? 'https://api.analytics.example.com';
}
async initialize() {
// Server-side services typically don't need initialization
}
async identify(userId: string, traits?: Record<string, string>) {
await fetch(`${this.endpoint}/identify`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ userId, traits }),
});
}
async trackPageView(path: string) {
await fetch(`${this.endpoint}/page`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ path }),
});
}
async trackEvent(
eventName: string,
eventProperties?: Record<string, string | string[]>
) {
await fetch(`${this.endpoint}/track`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${this.apiKey}`,
},
body: JSON.stringify({ event: eventName, properties: eventProperties }),
});
}
}

Environment Variables

Add your analytics credentials to your environment files:

apps/web/.env.local

# Google Analytics
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX
# Mixpanel
NEXT_PUBLIC_MIXPANEL_TOKEN=your-mixpanel-token
# PostHog
NEXT_PUBLIC_POSTHOG_KEY=phc_xxxxxxxxxxxxx
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com
# Server-side analytics
ANALYTICS_API_KEY=your-server-api-key

Conditional Provider Loading

To load providers based on environment:

packages/analytics/src/index.ts

import { createAnalyticsManager } from './analytics-manager';
import { NullAnalyticsService } from './null-analytics-service';
import type { AnalyticsManager, AnalyticsProviderFactory } from './types';
const providers: Record<string, AnalyticsProviderFactory<object>> = {
null: () => NullAnalyticsService,
};
// Only add Google Analytics if configured
if (process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID) {
const { GoogleAnalyticsService } = require('./google-analytics-service');
providers.google = () =>
new GoogleAnalyticsService({
measurementId: process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID,
});
}
export const analytics: AnalyticsManager = createAnalyticsManager({
providers,
});

Common Gotchas

  1. Browser-only code - Always check typeof window !== 'undefined' before accessing browser APIs in your service.
  2. Async initialization - The initialize() method is called immediately when the provider is registered. If you load external scripts, they may not be ready when the first tracking call happens.
  3. Event property types - The interface expects Record<string, string | string[]>. If your analytics service needs other types, convert them in your implementation.
  4. Server imports - Don't import client providers in server files. Keep separate provider configurations for client (index.ts) and server (server.ts).

Previous: Analytics Overview ←