# 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: ```typescript 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:** ```typescript 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:** ```typescript 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: ```typescript // In lib/services/create-services.ts const umamiEnabled = Boolean(process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID); const analytics = umamiEnabled ? new UmamiAnalyticsService({ enabled: true }) : new NoopAnalyticsService(); ``` ## Environment Variables ### Required for Umami ```bash NEXT_PUBLIC_UMAMI_WEBSITE_ID=59a7db94-0100-4c7e-98ef-99f45b17f9c3 ``` ### Optional (defaults provided) ```bash 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:** ```typescript 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:** ```typescript // Track current page service.trackPageview(); // Track custom URL service.trackPageview('/products/123?category=cables'); ``` ### UmamiAnalyticsService #### Constructor ```typescript new UmamiAnalyticsService(options: UmamiAnalyticsServiceOptions) ``` **Options:** - `enabled: boolean` - Whether analytics are enabled **Example:** ```typescript const service = new UmamiAnalyticsService({ enabled: true }); ``` ### NoopAnalyticsService #### Constructor ```typescript new NoopAnalyticsService() ``` **Example:** ```typescript const service = new NoopAnalyticsService(); ``` ## Type Definitions ### AnalyticsEventProperties ```typescript type AnalyticsEventProperties = Record< string, string | number | boolean | null | undefined >; ``` **Example:** ```typescript const properties: AnalyticsEventProperties = { product_id: '123', product_name: 'Cable', price: 99.99, quantity: 1, in_stock: true, discount: null, }; ``` ### UmamiAnalyticsServiceOptions ```typescript type UmamiAnalyticsServiceOptions = { enabled: boolean; }; ``` ## Best Practices ### 1. Use the Service Layer Always use the service layer instead of calling Umami directly: ```typescript // ✅ 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: ```typescript // ✅ 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: ```typescript 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: ```typescript // When NEXT_PUBLIC_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 ```typescript // __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: ```bash # 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 ```typescript // 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: ```typescript // 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: ```typescript // Services are not created until getAppServices() is called // This keeps initial bundle size minimal ``` ## Integration with Components ### Client Components ```typescript '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 ; } ``` ### Server Components ```typescript 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