feat(analytics): implement advanced tracking with script-less smart proxy
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 9s
Build & Deploy / 🧪 QA (push) Failing after 1m18s
Build & Deploy / 🏗️ Build (push) Failing after 2m58s
Build & Deploy / 🚀 Deploy (push) Has been skipped
Build & Deploy / 🩺 Health Check (push) Has been skipped
Build & Deploy / 🔔 Notify (push) Successful in 2s

- Added Umami Smart Proxy route handler
- Refactored Umami adapter to use proxy-based fetch
- Implemented TrackedButton, TrackedLink, and ScrollDepthTracker
- Integrated event tracking into ContactForm
- Enhanced Analytics component with manual pageview and performance tracking
This commit is contained in:
2026-02-16 23:03:42 +01:00
parent 7fd0c447bc
commit 2038b8fe47
13 changed files with 485 additions and 65 deletions

View File

@@ -96,7 +96,6 @@ export function getDefaultAnalytics(): AnalyticsService {
if (provider === "umami") {
defaultAnalytics = createUmamiAnalytics({
websiteId: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || "",
hostUrl: env.UMAMI_API_ENDPOINT,
});
} else if (provider === "plausible") {

View File

@@ -3,52 +3,76 @@
* Decoupled implementation
*/
import type { AnalyticsAdapter, AnalyticsEvent, AnalyticsConfig } from './interfaces';
import type {
AnalyticsAdapter,
AnalyticsEvent,
AnalyticsConfig,
} from "./interfaces";
export interface UmamiConfig extends AnalyticsConfig {
websiteId: string;
hostUrl?: string;
hostUrl?: string; // Optional, defaults to env var on server
}
export class UmamiAdapter implements AnalyticsAdapter {
private websiteId: string;
private hostUrl: string;
constructor(config: UmamiConfig) {
this.websiteId = config.websiteId;
this.hostUrl = config.hostUrl || 'https://cloud.umami.is';
this.hostUrl = config.hostUrl || "https://analytics.infra.mintel.me";
}
private async sendPayload(type: "event", data: Record<string, any>) {
try {
if (typeof window === "undefined") return;
const payload = {
hostname: window.location.hostname,
screen: `${window.screen.width}x${window.screen.height}`,
language: navigator.language,
referrer: document.referrer,
...data,
};
await fetch("/stats/api/send", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ type, payload }),
keepalive: true,
});
} catch (e) {
console.error("Failed to send analytics", e);
}
}
async track(event: AnalyticsEvent): Promise<void> {
if (typeof window === 'undefined') return;
if (typeof window === "undefined") return;
const w = window as any;
if (w.umami) {
w.umami.track(event.name, event.props);
}
}
async page(path: string, props?: Record<string, any>): Promise<void> {
// Umami tracks pageviews automatically by default,
// but we can manually trigger it if needed.
if (typeof window === 'undefined') return;
const w = window as any;
if (w.umami) {
w.umami.track(props?.name || 'pageview', { url: path, ...props });
}
}
async identify(userId: string, traits?: Record<string, any>): Promise<void> {
// Umami doesn't have a direct 'identify' like Segment,
// but we can track it as an event or session property if supported by the instance.
await this.track({
name: 'identify',
props: { userId, ...traits }
await this.sendPayload("event", {
name: event.name,
data: event.props,
url: window.location.pathname + window.location.search,
});
}
async page(path: string, props?: Record<string, any>): Promise<void> {
if (typeof window === "undefined") return;
await this.sendPayload("event", {
url: path,
...props,
});
}
async identify(
_userId: string,
_traits?: Record<string, any>,
): Promise<void> {
// Not implemented in this version
}
// No script tag needed for proxy mode
getScriptTag(): string {
return `<script async src="${this.hostUrl}/script.js" data-website-id="${this.websiteId}"></script>`;
return "";
}
}