feat: integrate Umami analytics for client-side and server-side event tracking

This commit is contained in:
2025-11-30 19:28:25 -05:00
parent e27877c477
commit 10d7500ef3
5 changed files with 93 additions and 0 deletions

View File

@@ -28,5 +28,8 @@ DB_DISABLE_SSL="true"
RESEND_API_KEY="your-resend-api-key"
RESEND_DOMAIN=""
# Analytics
NEXT_PUBLIC_UMAMI_WEBSITE_ID="your-website-id-here"
NEXT_PUBLIC_UMAMI_SCRIPT_URL="https://analytics.umami.is/script.js"
# Build tweaks
# SKIP_ENV_VALIDATION=1

View File

@@ -9,6 +9,7 @@ import { AnimationPreferencesProvider } from "~/components/providers/animation-p
import { ThemeProvider } from "~/components/providers/theme-provider";
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
import { UmamiScript } from "~/components/analytics/umami-script";
export const metadata: Metadata = {
title: "beenvoice - Invoicing Made Simple",
@@ -143,6 +144,7 @@ export default function RootLayout({
{children}
</AnimationPreferencesProvider>
<Toaster />
<UmamiScript />
</ColorThemeProvider>
</ThemeProvider>
</TRPCReactProvider>

View File

@@ -0,0 +1,19 @@
"use client";
import Script from "next/script";
import { env } from "~/env";
export function UmamiScript() {
if (!env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || !env.NEXT_PUBLIC_UMAMI_SCRIPT_URL) {
return null;
}
return (
<Script
defer
src={env.NEXT_PUBLIC_UMAMI_SCRIPT_URL}
data-website-id={env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
strategy="afterInteractive"
/>
);
}

View File

@@ -31,6 +31,8 @@ export const env = createEnv({
*/
client: {
NEXT_PUBLIC_APP_URL: z.string().url().optional(),
NEXT_PUBLIC_UMAMI_WEBSITE_ID: z.string().optional(),
NEXT_PUBLIC_UMAMI_SCRIPT_URL: z.string().url().optional(),
},
/**
@@ -46,6 +48,8 @@ export const env = createEnv({
NODE_ENV: process.env.NODE_ENV,
DB_DISABLE_SSL: process.env.DB_DISABLE_SSL,
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
NEXT_PUBLIC_UMAMI_WEBSITE_ID: process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
NEXT_PUBLIC_UMAMI_SCRIPT_URL: process.env.NEXT_PUBLIC_UMAMI_SCRIPT_URL,
},
/**
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially

65
src/server/umami.ts Normal file
View File

@@ -0,0 +1,65 @@
import { env } from "~/env";
type UmamiPayload = {
payload: {
hostname: string;
language: string;
referrer: string;
screen: string;
title: string;
url: string;
website: string;
name: string;
data?: Record<string, any>;
};
type: "event";
};
export async function trackServerEvent(
eventName: string,
eventData?: Record<string, any>,
) {
if (!env.NEXT_PUBLIC_UMAMI_WEBSITE_ID || !env.NEXT_PUBLIC_UMAMI_SCRIPT_URL) {
console.warn("Umami not configured, skipping server-side event tracking");
return;
}
// Extract API endpoint from script URL (e.g., https://analytics.umami.is/script.js -> https://analytics.umami.is/api/send)
const scriptUrl = new URL(env.NEXT_PUBLIC_UMAMI_SCRIPT_URL);
const apiUrl = `${scriptUrl.origin}/api/send`;
const payload: UmamiPayload = {
payload: {
hostname: env.NEXT_PUBLIC_APP_URL
? new URL(env.NEXT_PUBLIC_APP_URL).hostname
: "localhost",
language: "en-US",
referrer: "",
screen: "",
title: "Server Event",
url: "/",
website: env.NEXT_PUBLIC_UMAMI_WEBSITE_ID,
name: eventName,
data: eventData,
},
type: "event",
};
try {
const response = await fetch(apiUrl, {
method: "POST",
headers: {
"Content-Type": "application/json",
"User-Agent":
"Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)",
},
body: JSON.stringify(payload),
});
if (!response.ok) {
console.error("Failed to send Umami event:", await response.text());
}
} catch (error) {
console.error("Error sending Umami event:", error);
}
}