Files
at-mintel/packages/observability/src/analytics/umami.ts
Marc Mintel 61e78ea672
All checks were successful
Monorepo Pipeline / 🧪 Quality Assurance (push) Successful in 3m50s
Monorepo Pipeline / 🚀 Release (push) Successful in 3m38s
Monorepo Pipeline / 🐳 Build & Push Images (push) Successful in 7m6s
feat: integrate observability
2026-02-07 10:23:51 +01:00

116 lines
3.1 KiB
TypeScript

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<string, any>) {
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),
});
}
}