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
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:
70
apps/web/src/components/analytics/ScrollDepthTracker.tsx
Normal file
70
apps/web/src/components/analytics/ScrollDepthTracker.tsx
Normal 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;
|
||||
}
|
||||
48
apps/web/src/components/analytics/TrackedButton.tsx
Normal file
48
apps/web/src/components/analytics/TrackedButton.tsx
Normal 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} />;
|
||||
}
|
||||
45
apps/web/src/components/analytics/TrackedLink.tsx
Normal file
45
apps/web/src/components/analytics/TrackedLink.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
apps/web/src/components/analytics/analytics-events.ts
Normal file
44
apps/web/src/components/analytics/analytics-events.ts
Normal 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];
|
||||
40
apps/web/src/components/analytics/useAnalytics.ts
Normal file
40
apps/web/src/components/analytics/useAnalytics.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user