mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2025-12-13 01:24:44 -05:00
feat: integrate Umami analytics for client-side and server-side event tracking
This commit is contained in:
@@ -28,5 +28,8 @@ DB_DISABLE_SSL="true"
|
|||||||
RESEND_API_KEY="your-resend-api-key"
|
RESEND_API_KEY="your-resend-api-key"
|
||||||
RESEND_DOMAIN=""
|
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
|
# Build tweaks
|
||||||
# SKIP_ENV_VALIDATION=1
|
# SKIP_ENV_VALIDATION=1
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { AnimationPreferencesProvider } from "~/components/providers/animation-p
|
|||||||
|
|
||||||
import { ThemeProvider } from "~/components/providers/theme-provider";
|
import { ThemeProvider } from "~/components/providers/theme-provider";
|
||||||
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
|
import { ColorThemeProvider } from "~/components/providers/color-theme-provider";
|
||||||
|
import { UmamiScript } from "~/components/analytics/umami-script";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "beenvoice - Invoicing Made Simple",
|
title: "beenvoice - Invoicing Made Simple",
|
||||||
@@ -143,6 +144,7 @@ export default function RootLayout({
|
|||||||
{children}
|
{children}
|
||||||
</AnimationPreferencesProvider>
|
</AnimationPreferencesProvider>
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
<UmamiScript />
|
||||||
</ColorThemeProvider>
|
</ColorThemeProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</TRPCReactProvider>
|
</TRPCReactProvider>
|
||||||
|
|||||||
19
src/components/analytics/umami-script.tsx
Normal file
19
src/components/analytics/umami-script.tsx
Normal 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"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -31,6 +31,8 @@ export const env = createEnv({
|
|||||||
*/
|
*/
|
||||||
client: {
|
client: {
|
||||||
NEXT_PUBLIC_APP_URL: z.string().url().optional(),
|
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,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
DB_DISABLE_SSL: process.env.DB_DISABLE_SSL,
|
DB_DISABLE_SSL: process.env.DB_DISABLE_SSL,
|
||||||
NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL,
|
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
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
|
|||||||
65
src/server/umami.ts
Normal file
65
src/server/umami.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user