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. */ // eslint-disable-next-line @typescript-eslint/no-explicit-any 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 }), keepalive: true, // eslint-disable-next-line @typescript-eslint/no-explicit-any } 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), }); } }