import type { AnalyticsEventProperties, AnalyticsService } from './analytics-service'; import { config } from '../../config'; /** * Configuration options for UmamiAnalyticsService. * * @property enabled - Whether analytics are enabled */ export type UmamiAnalyticsServiceOptions = { enabled: boolean; }; /** * Umami Analytics Service Implementation (Script-less/Proxy edition). * * This version implements the Umami tracking protocol directly via fetch, * eliminating the need to load an external script.js file. * * In the browser, it gathers standard metadata (screen, language, referrer) * and sends it to the proxied '/stats/api/send' endpoint. */ export class UmamiAnalyticsService implements AnalyticsService { private websiteId?: string; private endpoint: string; constructor(private readonly options: UmamiAnalyticsServiceOptions) { this.websiteId = config.analytics.umami.websiteId; // On server, use the full internal URL; on client, use the proxied path this.endpoint = typeof window === 'undefined' ? config.analytics.umami.apiEndpoint : '/stats'; } /** * Internal method to send the payload to Umami API. */ private async sendPayload(type: 'event', data: Record) { if (!this.options.enabled || !this.websiteId) return; try { const payload = { website: this.websiteId, hostname: typeof window !== 'undefined' ? window.location.hostname : 'server', screen: typeof window !== 'undefined' ? `${window.screen.width}x${window.screen.height}` : undefined, language: typeof window !== 'undefined' ? navigator.language : undefined, referrer: typeof window !== 'undefined' ? document.referrer : undefined, ...data, }; const response = await fetch(`${this.endpoint}/api/send`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'User-Agent': typeof window === 'undefined' ? 'KLZ-Server' : navigator.userAgent, }, body: JSON.stringify({ type, payload }), // Use keepalive for page navigation events to ensure they complete keepalive: true, } as any); if (!response.ok && process.env.NODE_ENV === 'development') { const errorText = await response.text(); console.warn(`[Umami] API responded with ${response.status}: ${errorText}`); } } catch (error) { if (process.env.NODE_ENV === 'development') { console.error('[Umami] Failed to send analytics:', error); } } } /** * Track a custom event. */ track(eventName: string, props?: AnalyticsEventProperties) { this.sendPayload('event', { name: eventName, data: props, url: typeof window !== 'undefined' ? window.location.pathname + window.location.search : undefined, }); } /** * Track a pageview. */ trackPageview(url?: string) { this.sendPayload('event', { url: url || (typeof window !== 'undefined' ? window.location.pathname + window.location.search : undefined), }); } }