StackSee Analytics

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 gracefully

Why 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 event

Available Providers

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 logged

User 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 user

Shutdown (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 excluded
  • identify - User identification
  • track - Custom event tracking
  • pageView - Page view events
  • pageLeave - Page leave events
  • reset - 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
});

Next Steps