# 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
{data}
; } ``` ## Troubleshooting ### Analytics Not Working 1. **Check environment variables:** ```bash echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID ``` 2. **Verify service selection:** ```typescript 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 | ## Related Files - [`components/analytics/useAnalytics.ts`](../components/analytics/useAnalytics.ts) - Custom hook for easy event tracking - [`components/analytics/analytics-events.ts`](../components/analytics/analytics-events.ts) - Event definitions - [`components/analytics/UmamiScript.tsx`](../components/analytics/UmamiScript.tsx) - Script loader component - [`components/analytics/AnalyticsProvider.tsx`](../components/analytics/AnalyticsProvider.tsx) - Route change tracker - [`lib/services/create-services.ts`](../lib/services/create-services.ts) - Service factory ## 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.