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

@@ -0,0 +1,70 @@
"use client";
import { useEffect, useRef } from "react";
import { usePathname } from "next/navigation";
import { useAnalytics } from "./useAnalytics";
import { AnalyticsEvents } from "./analytics-events";
/**
* ScrollDepthTracker
* Tracks user scroll progress across pages.
* Fires events at 25%, 50%, 75%, and 100% depth.
*/
export function ScrollDepthTracker() {
const pathname = usePathname();
const { trackEvent } = useAnalytics();
const trackedDepths = useRef<Set<number>>(new Set());
// Reset tracking when path changes
useEffect(() => {
trackedDepths.current.clear();
}, [pathname]);
useEffect(() => {
const handleScroll = () => {
const scrollY = window.scrollY;
const windowHeight = window.innerHeight;
const documentHeight = document.documentElement.scrollHeight;
// Calculate how far the user has scrolled in percentage
// documentHeight - windowHeight is the total scrollable distance
const totalScrollable = documentHeight - windowHeight;
if (totalScrollable <= 0) return; // Not scrollable
const scrollPercentage = Math.round((scrollY / totalScrollable) * 100);
// We only care about specific milestones
const milestones = [25, 50, 75, 100];
milestones.forEach((milestone) => {
if (
scrollPercentage >= milestone &&
!trackedDepths.current.has(milestone)
) {
trackedDepths.current.add(milestone);
trackEvent(AnalyticsEvents.SCROLL_DEPTH, {
depth: milestone,
path: pathname,
});
}
});
};
// Use passive listener for better performance
window.addEventListener("scroll", handleScroll, { passive: true });
// Initial check (in case page is short or already scrolled)
if (document.readyState === "complete") {
handleScroll();
} else {
window.addEventListener("load", handleScroll);
}
return () => {
window.removeEventListener("scroll", handleScroll);
window.removeEventListener("load", handleScroll);
};
}, [pathname, trackEvent]);
return null;
}

View File

@@ -0,0 +1,48 @@
"use client";
import React from "react";
import { Button } from "../Button";
import { useAnalytics } from "./useAnalytics";
import { AnalyticsEvents } from "./analytics-events";
// Since ButtonProps is not exported from Button.tsx, we define a compatible interface
interface ButtonProps {
href: string; // Correctly matching Button.tsx which has href as required
children: React.ReactNode;
variant?: "primary" | "outline" | "ghost";
size?: "normal" | "large";
className?: string;
showArrow?: boolean;
[key: string]: any; // Allow other props
}
interface TrackedButtonProps extends ButtonProps {
eventName?: string;
eventProperties?: Record<string, any>;
onClick?: (e: React.MouseEvent<any>) => void;
}
/**
* A wrapper around the project's Button component that tracks click events.
*/
export function TrackedButton({
eventName = AnalyticsEvents.BUTTON_CLICK,
eventProperties = {},
onClick,
...props
}: TrackedButtonProps) {
const { trackEvent } = useAnalytics();
const handleClick = (e: React.MouseEvent<any>) => {
trackEvent(eventName, {
...eventProperties,
label:
typeof props.children === "string"
? props.children
: eventProperties.label,
});
if (onClick) onClick(e);
};
return <Button {...props} onClick={handleClick} />;
}

View File

@@ -0,0 +1,45 @@
"use client";
import React from "react";
import Link, { LinkProps } from "next/link";
import { useAnalytics } from "./useAnalytics";
import { AnalyticsEvents } from "./analytics-events";
interface TrackedLinkProps extends LinkProps {
eventName?: string;
eventProperties?: Record<string, any>;
className?: string;
children: React.ReactNode;
onClick?: (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => void;
// allow passing other props
[key: string]: any;
}
/**
* A wrapper around next/link that tracks the click event.
*/
export function TrackedLink({
href,
eventName = AnalyticsEvents.LINK_CLICK,
eventProperties = {},
className,
children,
onClick,
...props
}: TrackedLinkProps) {
const { trackEvent } = useAnalytics();
const handleClick = (e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
trackEvent(eventName, {
href: href.toString(),
...eventProperties,
});
if (onClick) onClick(e);
};
return (
<Link href={href} className={className} onClick={handleClick} {...props}>
{children}
</Link>
);
}

View File

@@ -0,0 +1,44 @@
/**
* Analytics Events Utility
*
* Centralized definitions for common analytics events and their properties.
*/
export const AnalyticsEvents = {
// Page & Navigation Events
PAGE_VIEW: "pageview",
PAGE_SCROLL: "page_scroll",
PAGE_EXIT: "page_exit",
SCROLL_DEPTH: "scroll_depth",
// User Interaction Events
BUTTON_CLICK: "button_click",
LINK_CLICK: "link_click",
FORM_SUBMIT: "form_submit",
FORM_START: "form_start",
FORM_ERROR: "form_error",
FORM_FIELD_FOCUS: "form_field_focus",
// UI Interaction Events
MODAL_OPEN: "modal_open",
MODAL_CLOSE: "modal_close",
TOGGLE_SWITCH: "toggle_switch",
ACCORDION_TOGGLE: "accordion_toggle",
TAB_SWITCH: "tab_switch",
// Error & Performance Events
ERROR: "error",
PERFORMANCE: "performance",
API_ERROR: "api_error",
API_SUCCESS: "api_success",
// Custom Business Events
CONTACT_FORM_SUBMIT: "contact_form_submit",
NEWSLETTER_SUBSCRIBE: "newsletter_subscribe",
} as const;
/**
* Type-safe event properties for common events
*/
export type AnalyticsEventName =
(typeof AnalyticsEvents)[keyof typeof AnalyticsEvents];

View File

@@ -0,0 +1,40 @@
"use client";
import { useCallback } from "react";
import { getDefaultAnalytics } from "../../utils/analytics";
import type { AnalyticsEventName } from "./analytics-events";
/**
* Custom hook for tracking analytics events.
* Wraps the analytics service for easy use in components.
*/
export function useAnalytics() {
const trackEvent = useCallback(
(
eventName: string | AnalyticsEventName,
properties?: Record<string, any>,
) => {
const analytics = getDefaultAnalytics();
analytics.trackEvent(eventName, properties);
if (process.env.NODE_ENV === "development") {
console.debug("[Analytics] Tracked event:", eventName, properties);
}
},
[],
);
const trackPageview = useCallback((url?: string) => {
const analytics = getDefaultAnalytics();
analytics.page(url || window.location.pathname);
if (process.env.NODE_ENV === "development") {
console.debug("[Analytics] Tracked pageview:", url ?? "current location");
}
}, []);
return {
trackEvent,
trackPageview,
};
}