- Added 5s timeout to GotifyNotificationService - Reduced timeout to 2s in UmamiAnalyticsService - Implemented non-blocking analytics tracking in layout using Next.js after() API
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.NEXT_PUBLIC_UMAMI_WEBSITE_ID);
const analytics = umamiEnabled
? new UmamiAnalyticsService({ enabled: true })
: new NoopAnalyticsService();
Environment Variables
Required for Umami
NEXT_PUBLIC_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 trackprops- 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 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
// __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:
- Environment Detection - Checks for browser environment
- Service Availability - Checks if Umami is loaded
- 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
-
Check environment variables:
echo $NEXT_PUBLIC_UMAMI_WEBSITE_ID -
Verify service selection:
import { getAppServices } from '@/lib/services/create-services'; const services = getAppServices(); console.log(services.analytics); // Should be UmamiAnalyticsService -
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- Custom hook for easy event trackingcomponents/analytics/analytics-events.ts- Event definitionscomponents/analytics/UmamiScript.tsx- Script loader componentcomponents/analytics/AnalyticsProvider.tsx- Route change trackerlib/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.