Files
klz-cables.com/lib/services/analytics/README.md
Marc Mintel c646815a3a
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 1m14s
Build & Deploy / 🧪 QA (push) Successful in 3m20s
Build & Deploy / 🧪 Smoke Test (push) Failing after 49s
Build & Deploy / ⚡ Lighthouse (push) Successful in 3m24s
Build & Deploy / 🏗️ Build (push) Successful in 3m2s
Build & Deploy / 🚀 Deploy (push) Successful in 26s
Build & Deploy / 🔔 Notify (push) Successful in 2s
chore(analytics): completely scrub NEXT_PUBLIC prefix from umami website id across codebase and docs
2026-02-20 15:29:50 +01:00

10 KiB

Analytics Service Layer

This directory contains the service layer implementation for analytics tracking in the KLZ Cables application.

Overview

The analytics service layer provides a clean abstraction over different analytics implementations (Umami, Google Analytics, etc.) while maintaining a consistent API.

Architecture

lib/services/analytics/
├── analytics-service.ts          # Interface definition
├── umami-analytics-service.ts    # Umami implementation
├── noop-analytics-service.ts     # No-op fallback implementation
└── README.md                     # This file

Components

1. AnalyticsService Interface (analytics-service.ts)

Defines the contract for all analytics services:

export interface AnalyticsService {
  track(eventName: string, props?: AnalyticsEventProperties): void;
  trackPageview(url?: string): void;
}

Key Features:

  • Type-safe event properties
  • Consistent API across implementations
  • Well-documented with JSDoc comments

2. UmamiAnalyticsService (umami-analytics-service.ts)

Implements the AnalyticsService interface for Umami analytics.

Features:

  • Type-safe event tracking
  • Automatic pageview tracking
  • Browser environment detection
  • Graceful error handling
  • Comprehensive JSDoc documentation

Usage:

import { UmamiAnalyticsService } from '@/lib/services/analytics/umami-analytics-service';

const service = new UmamiAnalyticsService({ enabled: true });
service.track('button_click', { button_id: 'cta' });
service.trackPageview('/products/123');

3. NoopAnalyticsService (noop-analytics-service.ts)

A no-op implementation used as a fallback when analytics are disabled.

Features:

  • Maintains the same API as other services
  • Safe to call even when analytics are disabled
  • No performance impact
  • Comprehensive JSDoc documentation

Usage:

import { NoopAnalyticsService } from '@/lib/services/analytics/noop-analytics-service';

const service = new NoopAnalyticsService();
service.track('button_click', { button_id: 'cta' }); // Does nothing
service.trackPageview('/products/123'); // Does nothing

Service Selection

The service layer automatically selects the appropriate implementation based on environment variables:

// In lib/services/create-services.ts
const umamiEnabled = Boolean(process.env.UMAMI_WEBSITE_ID);

const analytics = umamiEnabled
  ? new UmamiAnalyticsService({ enabled: true })
  : new NoopAnalyticsService();

Environment Variables

Required for Umami

UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3

Optional (defaults provided)

NEXT_PUBLIC_UMAMI_SCRIPT_URL=https://analytics.infra.mintel.me/script.js

API Reference

AnalyticsService Interface

track(eventName: string, props?: AnalyticsEventProperties): void

Track a custom event with optional properties.

Parameters:

  • eventName - The name of the event to track
  • props - Optional event properties (metadata)

Example:

service.track('product_add_to_cart', {
  product_id: '123',
  product_name: 'Cable',
  price: 99.99,
  quantity: 1,
});

trackPageview(url?: string): void

Track a pageview.

Parameters:

  • url - The URL to track (defaults to current location)

Example:

// Track current page
service.trackPageview();

// Track custom URL
service.trackPageview('/products/123?category=cables');

UmamiAnalyticsService

Constructor

new UmamiAnalyticsService(options: UmamiAnalyticsServiceOptions)

Options:

  • enabled: boolean - Whether analytics are enabled

Example:

const service = new UmamiAnalyticsService({ enabled: true });

NoopAnalyticsService

Constructor

new NoopAnalyticsService();

Example:

const service = new NoopAnalyticsService();

Type Definitions

AnalyticsEventProperties

type AnalyticsEventProperties = Record<string, string | number | boolean | null | undefined>;

Example:

const properties: AnalyticsEventProperties = {
  product_id: '123',
  product_name: 'Cable',
  price: 99.99,
  quantity: 1,
  in_stock: true,
  discount: null,
};

UmamiAnalyticsServiceOptions

type UmamiAnalyticsServiceOptions = {
  enabled: boolean;
};

Best Practices

1. Use the Service Layer

Always use the service layer instead of calling Umami directly:

// ✅ Good
import { getAppServices } from '@/lib/services/create-services';

const services = getAppServices();
services.analytics.track('button_click', { button_id: 'cta' });

// ❌ Avoid
(window as any).umami?.track('button_click', { button_id: 'cta' });

2. Check Environment

The service layer automatically handles environment detection:

// ✅ Safe - works in both server and client
const services = getAppServices();
services.analytics.track('event', { prop: 'value' });

// ❌ Unsafe - may fail in server environment
if (typeof window !== 'undefined') {
  window.umami?.track('event', { prop: 'value' });
}

3. Use Type-Safe Events

Import events from the centralized definitions:

import { AnalyticsEvents } from '@/components/analytics/analytics-events';

// ✅ Type-safe
services.analytics.track(AnalyticsEvents.BUTTON_CLICK, {
  button_id: 'cta',
});

// ❌ Prone to typos
services.analytics.track('button_click', {
  button_id: 'cta',
});

4. Handle Disabled Analytics

The service layer gracefully handles disabled analytics:

// When UMAMI_WEBSITE_ID is not set:
// - NoopAnalyticsService is used
// - All calls are safe (no-op)
// - No errors are thrown

const services = getAppServices();
services.analytics.track('event', { prop: 'value' }); // Safe, does nothing

Testing

Mocking for Tests

// __tests__/analytics-mock.ts
export const mockAnalytics = {
  track: jest.fn(),
  trackPageview: jest.fn(),
};

jest.mock('@/lib/services/create-services', () => ({
  getAppServices: () => ({
    analytics: mockAnalytics,
  }),
}));

// Usage in tests
import { mockAnalytics } from './analytics-mock';

test('tracks button click', () => {
  // ... test code ...
  expect(mockAnalytics.track).toHaveBeenCalledWith('button_click', {
    button_id: 'cta',
  });
});

Development Mode

In development, the service layer logs to console:

# Console output:
[Umami] Tracked event: button_click { button_id: 'cta' }
[Umami] Tracked pageview: /products/123

Error Handling

The service layer includes built-in error handling:

  1. Environment Detection - Checks for browser environment
  2. Service Availability - Checks if Umami is loaded
  3. Graceful Degradation - Falls back to NoopAnalyticsService if needed
// These are all safe:
const services = getAppServices();
services.analytics.track('event', { prop: 'value' }); // Works or does nothing
services.analytics.trackPageview('/path'); // Works or does nothing

Performance

Singleton Pattern

The service layer uses a singleton pattern for performance:

// First call creates the singleton
const services1 = getAppServices();

// Subsequent calls return the cached singleton
const services2 = getAppServices();

// services1 === services2 (same instance)

Lazy Initialization

Services are only created when first accessed:

// Services are not created until getAppServices() is called
// This keeps initial bundle size minimal

Integration with Components

Client Components

'use client';

import { getAppServices } from '@/lib/services/create-services';

function MyComponent() {
  const handleClick = () => {
    const services = getAppServices();
    services.analytics.track('button_click', { button_id: 'my-button' });
  };

  return <button onClick={handleClick}>Click Me</button>;
}

Server Components

import { getAppServices } from '@/lib/services/create-services';

async function MyServerComponent() {
  const services = getAppServices();

  // Note: Analytics won't work in server components
  // Use client components for analytics tracking
  // But you can still access other services like cache

  const data = await services.cache.get('key');

  return <div>{data}</div>;
}

Troubleshooting

Analytics Not Working

  1. Check environment variables:

    echo $UMAMI_WEBSITE_ID
    
  2. Verify service selection:

    import { getAppServices } from '@/lib/services/create-services';
    
    const services = getAppServices();
    console.log(services.analytics); // Should be UmamiAnalyticsService
    
  3. Check Umami dashboard:

    • Log into Umami
    • Verify website ID matches
    • Check if data is being received

Common Issues

Issue Solution
No data in Umami Check website ID and script URL
Events not tracking Verify service is being used
Script not loading Check network connection, CORS
Wrong data Verify event properties are correct

Summary

The analytics service layer provides:

  • Type-safe API - TypeScript throughout
  • Clean abstraction - Easy to switch analytics providers
  • Graceful degradation - Safe no-op fallback
  • Comprehensive documentation - JSDoc comments and examples
  • Performance optimized - Singleton pattern, lazy initialization
  • Error handling - Safe in all environments

This layer is the foundation for all analytics tracking in the application.