import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service'; /** * Type definition for the Umami global object. * * This represents the `window.umami` object that the Umami script exposes. * The `track` function can accept either an event name or a URL. */ type UmamiGlobal = { track?: (eventOrUrl: string, props?: AnalyticsEventProperties) => void; }; /** * Configuration options for UmamiAnalyticsService. * * @property enabled - Whether analytics are enabled */ export type UmamiAnalyticsServiceOptions = { enabled: boolean; }; /** * Umami Analytics Service Implementation. * * This service implements the AnalyticsService interface for Umami analytics. * It provides type-safe event tracking and pageview tracking. * * @example * ```typescript * // Service creation (usually done by create-services.ts) * const service = new UmamiAnalyticsService({ enabled: true }); * * // Track events * service.track('button_click', { button_id: 'cta' }); * service.trackPageview('/products/123'); * ``` * * @example * ```typescript * // Using through the service layer (recommended) * import { getAppServices } from '@/lib/services/create-services'; * * const services = getAppServices(); * services.analytics.track('product_add_to_cart', { * product_id: '123', * price: 99.99, * }); * ``` */ export class UmamiAnalyticsService implements AnalyticsService { constructor(private readonly options: UmamiAnalyticsServiceOptions) {} /** * Track a custom event with optional properties. * * This method checks if analytics are enabled and if we're in a browser environment * before attempting to track the event. * * @param eventName - The name of the event to track * @param props - Optional event properties * * @example * ```typescript * service.track('product_add_to_cart', { * product_id: '123', * product_name: 'Cable', * price: 99.99, * quantity: 1, * }); * ``` */ track(eventName: string, props?: AnalyticsEventProperties) { if (!this.options.enabled) return; // Server-side tracking via proxy if (typeof window === 'undefined') { const { getServerAppServices } = require('../create-services.server'); const { config } = require('../../config'); const websiteId = config.analytics.umami.websiteId; const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', ''); if (!websiteId) return; const logger = getServerAppServices().logger.child({ component: 'analytics' }); logger.info('Sending analytics event', { eventName, props }); fetch(`${umamiUrl}/api/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' }, body: JSON.stringify({ type: 'event', payload: { website: websiteId, name: eventName, data: props } }), }).catch((error) => { logger.error('Failed to send analytics event', { eventName, props, error }); }); return; } const umami = (window as unknown as { umami?: UmamiGlobal }).umami; umami?.track?.(eventName, props); } /** * Track a pageview. * * This method checks if analytics are enabled and if we're in a browser environment * before attempting to track the pageview. * * Umami treats `track(url)` as a pageview override, so we can use the same * `track` function for both events and pageviews. * * @param 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'); * ``` */ trackPageview(url?: string) { if (!this.options.enabled) return; // Server-side tracking via proxy if (typeof window === 'undefined') { const { getServerAppServices } = require('../create-services.server'); const { config } = require('../../config'); const websiteId = config.analytics.umami.websiteId; const umamiUrl = config.analytics.umami.scriptUrl.replace('/script.js', ''); if (!websiteId || !url) return; const logger = getServerAppServices().logger.child({ component: 'analytics' }); logger.info('Sending analytics pageview', { url }); fetch(`${umamiUrl}/api/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': 'KLZ-Server' }, body: JSON.stringify({ type: 'event', payload: { website: websiteId, url } }), }).catch((error) => { logger.error('Failed to send analytics pageview', { url, error }); }); return; } const umami = (window as unknown as { umami?: UmamiGlobal }).umami; // Umami treats `track(url)` as a pageview override. if (url) umami?.track?.(url); else umami?.track?.(window.location.pathname + window.location.search); } }