import type { AnalyticsService, AnalyticsEventProperties } from "./service"; export interface UmamiConfig { websiteId?: string; apiEndpoint: string; // The endpoint to send to (proxied or direct) enabled: boolean; } export interface Logger { debug(msg: string, data?: any): void; warn(msg: string, data?: any): void; error(msg: string, data?: any): void; trace(msg: string, data?: any): void; } /** * Umami Analytics Service Implementation (Script-less/Proxy edition). */ export class UmamiAnalyticsService implements AnalyticsService { private logger?: Logger; constructor( private config: UmamiConfig, logger?: Logger, ) { this.logger = logger; } private async sendPayload(type: "event", data: Record) { if (!this.config.enabled) return; const isClient = typeof window !== "undefined"; const websiteId = this.config.websiteId; if (!isClient && !websiteId) { this.logger?.warn( "Umami tracking called on server but no Website ID configured", ); return; } try { const payload = { website: websiteId, hostname: isClient ? window.location.hostname : "server", screen: isClient ? `${window.screen.width}x${window.screen.height}` : undefined, language: isClient ? navigator.language : undefined, referrer: isClient ? document.referrer : undefined, ...data, }; this.logger?.trace("Sending analytics payload", { type, url: data.url }); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); try { const response = await fetch(`${this.config.apiEndpoint}/api/send`, { method: "POST", headers: { "Content-Type": "application/json", "User-Agent": isClient ? navigator.userAgent : "Mintel-Server", }, body: JSON.stringify({ type, payload }), keepalive: true, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { const errorText = await response.text(); this.logger?.warn("Umami API responded with error", { status: response.status, error: errorText.slice(0, 100), }); } } catch (fetchError) { clearTimeout(timeoutId); if ((fetchError as Error).name === "AbortError") { this.logger?.error("Umami request timed out"); } else { throw fetchError; } } } catch (error) { this.logger?.error("Failed to send analytics", { error: (error as Error).message, }); } } track(eventName: string, props?: AnalyticsEventProperties) { this.sendPayload("event", { name: eventName, data: props, url: typeof window !== "undefined" ? window.location.pathname + window.location.search : undefined, }); } trackPageview(url?: string) { this.sendPayload("event", { url: url || (typeof window !== "undefined" ? window.location.pathname + window.location.search : undefined), }); } }