Providers
Understanding the plugin architecture that powers event delivery
Providers are plugins that send your events to analytics services. The library uses a flexible plugin architecture that allows you to use any analytics service—or multiple services simultaneously.
What Are Providers?
A provider is a small adapter that translates your events into the format expected by an analytics service:
import { createClientAnalytics } from '@stacksee/analytics/client';
import { PostHogClientProvider } from '@stacksee/analytics/providers/client';
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' })
]
});
// You call track()
analytics.track('user_signed_up', { email: 'user@example.com' });
// Provider handles the rest:
// 1. Transforms event to PostHog format
// 2. Sends to PostHog servers
// 3. Handles errors gracefullyWhy Use Providers?
1. Switch Services Without Code Changes
Your tracking code stays the same, just swap providers:
// Week 1: Using PostHog
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' })
]
});
// Week 2: Switched to Pirsch - same tracking code!
const analytics = createClientAnalytics({
providers: [
new PirschClientProvider({ identificationCode: 'xxx' })
]
});
// Your components don't change at all
analytics.track('button_clicked', { id: 'cta' });2. Use Multiple Services
Send events to multiple analytics services with one call:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' }), // Product analytics
// Bento doesn't support anonymous page views - exclude pageView by default
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }), // Email marketing
exclude: ['pageView']
},
new PirschClientProvider({ identificationCode: 'xxx' }) // Privacy-focused analytics
]
});
// All three services receive this event
analytics.track('user_signed_up', {
email: 'user@example.com',
plan: 'pro'
});3. Graceful Error Handling
If one provider fails, others continue working:
// PostHog is down, but other providers still receive events
analytics.track('purchase', { amount: 99.99 });
// ❌ PostHog fails silently
// ✅ Bento receives event
// ✅ Pirsch receives eventAvailable Providers
PostHog
Product analytics with feature flags and session replay
Bento
Email marketing and automation with event tracking
Pirsch
Privacy-focused, cookie-free web analytics
Proxy Provider
Route events through your server to bypass ad-blockers
Custom Providers
Create your own provider for any analytics service
Provider Lifecycle
All providers implement a standard interface:
interface AnalyticsProvider {
// Initialize the provider (load SDK, connect, etc.)
initialize(): Promise<void> | void;
// Track a custom event
track(event: BaseEvent, context?: EventContext): Promise<void> | void;
// Identify a user
identify(userId: string, traits?: Record<string, unknown>): Promise<void> | void;
// Track page view
pageView(properties?: Record<string, unknown>, context?: EventContext): Promise<void> | void;
// Track page leave
pageLeave(properties?: Record<string, unknown>, context?: EventContext): Promise<void> | void;
// Reset user session (logout)
reset(): Promise<void> | void;
// Cleanup (server-side only)
shutdown?(): Promise<void>;
}Initialization
Providers are initialized automatically when you create an analytics instance:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' })
]
});
// PostHogClientProvider.initialize() is called automatically
await analytics.initialize();You can also initialize manually if needed:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' })
]
});
// Initialize later
await analytics.initialize();Tracking Events
When you call track(), all providers receive the event:
analytics.track('button_clicked', {
buttonId: 'cta',
location: 'hero'
});
// Behind the scenes:
// 1. Event is normalized
// 2. Each provider's track() method is called
// 3. Providers transform and send the event
// 4. Errors are caught and loggedUser Identification
The identify() method tells providers who the current user is:
analytics.identify('user-123', {
email: 'user@example.com',
name: 'John Doe',
plan: 'pro'
});
// All providers are notified:
// - PostHog creates/updates user profile
// - Bento adds/updates subscriber (requires email)
// - Pirsch associates future events with this userShutdown (Server-Side)
Server providers must be shut down to flush pending events:
import { serverAnalytics } from '@/lib/server-analytics';
// Track some events
await serverAnalytics.track('api_request', { endpoint: '/users' });
// IMPORTANT: Always shutdown in serverless environments
await serverAnalytics.shutdown();Without shutdown(), events may be lost when the serverless function terminates.
Provider Routing
Sometimes you want to use a provider for specific methods only. For example, you might want Bento for email marketing (identify, track) but not for page views, or use different providers for different types of events.
Important: Bento requires an email address for all events and does not support anonymous tracking. Always exclude pageView from Bento unless you're certain all visitors are identified before page views are tracked.
Selective Method Routing
Include specific methods:
const analytics = createClientAnalytics({
providers: [
// Use Bento only for identify and track
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
methods: ['identify', 'track']
}
]
});
// ✅ Called on Bento
analytics.identify('user-123');
analytics.track('purchase', { amount: 99 });
// ❌ NOT called on Bento
analytics.pageView();
analytics.pageLeave();Exclude specific methods:
const analytics = createClientAnalytics({
providers: [
// Use Bento for everything except pageView
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
exclude: ['pageView']
}
]
});
// ✅ Called on Bento
analytics.identify('user-123');
analytics.track('purchase', { amount: 99 });
analytics.pageLeave();
// ❌ NOT called on Bento
analytics.pageView();Mixed Provider Configuration
Combine simple and routed providers for maximum flexibility:
const analytics = createClientAnalytics({
providers: [
// PostHog gets all events
new PostHogClientProvider({ token: 'xxx' }),
// Google Analytics only gets page views
{
provider: new GoogleAnalyticsProvider({ measurementId: 'xxx' }),
methods: ['pageView']
},
// Bento gets everything except page views
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
exclude: ['pageView']
},
// Custom CRM provider only gets identify calls
{
provider: new CustomCRMProvider({ apiKey: 'xxx' }),
methods: ['identify']
}
]
});Available Methods
You can route these methods:
initialize- Always called, cannot be excludedidentify- User identificationtrack- Custom event trackingpageView- Page view eventspageLeave- Page leave eventsreset- Clear user session
Note: initialize() is always called on all providers regardless of routing configuration, as providers must be initialized before they can function.
Use Cases
Reduce noise in specific providers:
// Bento is for email automation - exclude page views (they don't support anonymous tracking)
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
exclude: ['pageView']
}Specialized provider roles:
const analytics = createClientAnalytics({
providers: [
// Full-featured analytics
new PostHogClientProvider({ token: 'xxx' }),
// CRM only needs user data
{
provider: new HubSpotProvider({ apiKey: 'xxx' }),
methods: ['identify']
},
// Email marketing needs identity and conversion events (exclude page views)
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
exclude: ['pageView'] // or use methods: ['identify', 'track'] to be explicit
}
]
});Custom provider implementation:
When building custom providers, routing lets you implement only what you need without making noop methods:
// Instead of implementing all methods with empty bodies...
class CustomProvider extends BaseAnalyticsProvider {
identify(userId: string) { /* actually implemented */ }
track(event: BaseEvent) { /* actually implemented */ }
pageView() { /* empty - not supported */ }
pageLeave() { /* empty - not supported */ }
reset() { /* empty - not supported */ }
}
// ...just implement what you need and use routing
class CustomProvider extends BaseAnalyticsProvider {
identify(userId: string) { /* implemented */ }
track(event: BaseEvent) { /* implemented */ }
// Don't need to implement these
pageView() {}
pageLeave() {}
reset() {}
}
// Then configure routing
{
provider: new CustomProvider(),
methods: ['identify', 'track']
}Using Multiple Providers
Common Combinations
Product Analytics + Email Marketing:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' }), // Track all behavior
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }), // Email automation
exclude: ['pageView'] // Bento requires email, skip anonymous page views
}
]
});Privacy-Focused + Full-Featured:
const analytics = createClientAnalytics({
providers: [
new PirschClientProvider({ identificationCode: 'xxx' }), // GDPR-compliant
new PostHogClientProvider({ token: 'xxx' }) // Full features
]
});Client-Side + Server-Side Proxy:
const analytics = createClientAnalytics({
providers: [
new ProxyClientProvider({
endpoint: '/api/analytics' // Bypass ad-blockers
})
]
});Conditional Providers
Enable providers based on environment or user consent:
const providers = [];
// Always use privacy-focused analytics
providers.push(new PirschClientProvider({ identificationCode: 'xxx' }));
// Only add PostHog if user consented
if (userConsent.analytics) {
providers.push(new PostHogClientProvider({ token: 'xxx' }));
}
// Only use Bento in production (exclude page views by default)
if (process.env.NODE_ENV === 'production') {
providers.push({
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
exclude: ['pageView']
});
}
const analytics = createClientAnalytics({ providers });Environment-Specific Imports
Always import providers from the correct path to avoid bundling issues:
// ✅ Correct - only browser code
import { createClientAnalytics } from '@stacksee/analytics/client';
import { PostHogClientProvider } from '@stacksee/analytics/providers/client';
// ❌ Wrong - may bundle Node.js code
import { PostHogClientProvider } from '@stacksee/analytics/providers';// ✅ Correct - only Node.js code
import { createServerAnalytics } from '@stacksee/analytics/server';
import { PostHogServerProvider } from '@stacksee/analytics/providers/server';
// ❌ Wrong - may bundle browser code
import { PostHogServerProvider } from '@stacksee/analytics/providers';Provider Configuration
Common Options
Most providers support these options:
new PostHogClientProvider({
token: 'xxx',
// Enable debug logging
debug: process.env.NODE_ENV === 'development',
// Disable the provider
enabled: !!process.env.POSTHOG_TOKEN,
// Custom API endpoint
api_host: 'https://analytics.example.com'
})Provider-Specific Options
Each provider has its own configuration options. See the provider guides for details:
Error Handling
Providers handle errors gracefully:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({ token: 'xxx' }),
{
provider: new BentoClientProvider({ siteUuid: 'xxx' }),
exclude: ['pageView'] // Recommended for Bento
}
]
});
// If PostHog fails, Bento still receives the event
analytics.track('user_signed_up', { email: 'user@example.com' });Enable debug mode to see errors:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({
token: 'xxx',
debug: true // Logs errors to console
})
],
debug: true // Logs all analytics activity
});Best Practices
1. Use Environment Variables
Never hardcode API keys:
// ✅ Good
new PostHogClientProvider({
token: import.meta.env.VITE_POSTHOG_KEY // Vite
// OR
token: process.env.NEXT_PUBLIC_POSTHOG_KEY // Next.js
})
// ❌ Bad
new PostHogClientProvider({
token: 'phc_1234567890' // Hardcoded!
})2. Initialize Once
Create your analytics instance once and reuse it:
// ✅ Good - lib/analytics.ts
export const analytics = createClientAnalytics({ /* ... */ });
// components/Button.tsx
import { analytics } from '@/lib/analytics';
// ❌ Bad - components/Button.tsx
function Button() {
const analytics = createClientAnalytics({ /* ... */ }); // Recreated every render!
}3. Graceful Degradation
Make providers optional:
const providers = [];
if (process.env.VITE_POSTHOG_KEY) {
providers.push(new PostHogClientProvider({
token: process.env.VITE_POSTHOG_KEY
}));
}
const analytics = createClientAnalytics({
providers: providers.length > 0 ? providers : [new NoOpProvider()]
});4. Test with Debug Mode
Enable debug mode during development:
const analytics = createClientAnalytics({
providers: [
new PostHogClientProvider({
token: 'xxx',
debug: import.meta.env.DEV
})
],
debug: import.meta.env.DEV
});