feat(analytics): Add Firebase Analytics integration with page tracking
- Initialize Firebase Analytics with configuration and client-side setup - Create GoogleAnalytics component to track page views on route changes - Add analytics utility functions for tracking custom events (button clicks, form submissions, link clicks, searches) - Integrate GoogleAnalytics component into home, not-found, and marketing layouts with Suspense boundary - Add firebase dependency to package.json - Enable automatic page view tracking with pathname and search parameters
This commit is contained in:
@@ -5,6 +5,7 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { ToastContainer, Zoom } from "react-toastify";
|
import { ToastContainer, Zoom } from "react-toastify";
|
||||||
|
import GoogleAnalytics from "../_components/GoogleAnalytics";
|
||||||
import NavigationBar from "../_components/NavigationBar";
|
import NavigationBar from "../_components/NavigationBar";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
@@ -136,6 +137,10 @@ export default async function RootLayout({
|
|||||||
pauseOnHover
|
pauseOnHover
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<GoogleAnalytics />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
<div className="container px-4 m-auto flex-grow">{children}</div>
|
<div className="container px-4 m-auto flex-grow">{children}</div>
|
||||||
<footer>
|
<footer>
|
||||||
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center ">
|
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center ">
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import Link from "next/link";
|
|||||||
import { ToastContainer, Zoom } from "react-toastify";
|
import { ToastContainer, Zoom } from "react-toastify";
|
||||||
|
|
||||||
import "../../globals.css";
|
import "../../globals.css";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import GoogleAnalytics from "@/app/_components/GoogleAnalytics";
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -126,6 +128,10 @@ export default async function RootLayout({
|
|||||||
pauseOnHover
|
pauseOnHover
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<GoogleAnalytics />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
<main className="container px-4 py-4 m-auto flex-grow">{children}</main>
|
<main className="container px-4 py-4 m-auto flex-grow">{children}</main>
|
||||||
<footer className="relative h-full bg-[url('/footer-bg.svg')] bg-cover bg-no-repeat mt-20">
|
<footer className="relative h-full bg-[url('/footer-bg.svg')] bg-cover bg-no-repeat mt-20">
|
||||||
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center">
|
<div className="relative z-10 container px-4 py-4 m-auto flex flex-col justify-center items-center">
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { analytics } from "@/lib/firebase";
|
||||||
|
import { logEvent } from "firebase/analytics";
|
||||||
|
import { usePathname, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function GoogleAnalytics() {
|
||||||
|
const pathname = usePathname();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (analytics) {
|
||||||
|
const url =
|
||||||
|
pathname +
|
||||||
|
(searchParams?.toString() ? `?${searchParams.toString()}` : "");
|
||||||
|
|
||||||
|
logEvent(analytics, "page_view", {
|
||||||
|
page_path: url,
|
||||||
|
page_title: document.title,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [pathname, searchParams]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import type { Metadata } from "next";
|
|||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import { ToastContainer, Zoom } from "react-toastify";
|
import { ToastContainer, Zoom } from "react-toastify";
|
||||||
import "../globals.css";
|
import "../globals.css";
|
||||||
|
import { Suspense } from "react";
|
||||||
|
import GoogleAnalytics from "../_components/GoogleAnalytics";
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-geist-sans",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
@@ -43,6 +45,10 @@ export default async function RootLayout({
|
|||||||
pauseOnHover
|
pauseOnHover
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Suspense>
|
||||||
|
<GoogleAnalytics />
|
||||||
|
</Suspense>
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { logEvent } from "firebase/analytics";
|
||||||
|
import { analytics } from "./firebase";
|
||||||
|
|
||||||
|
// Custom event tracking functions
|
||||||
|
export const trackEvent = (
|
||||||
|
eventName: string,
|
||||||
|
eventParams?: Record<string, any>
|
||||||
|
) => {
|
||||||
|
if (analytics) {
|
||||||
|
logEvent(analytics, eventName, eventParams);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Common event trackers
|
||||||
|
export const trackButtonClick = (buttonName: string, location?: string) => {
|
||||||
|
trackEvent("button_click", {
|
||||||
|
button_name: buttonName,
|
||||||
|
location: location || "unknown",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackFormSubmit = (formName: string, success: boolean) => {
|
||||||
|
trackEvent("form_submit", {
|
||||||
|
form_name: formName,
|
||||||
|
success,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackLinkClick = (linkUrl: string, linkText?: string) => {
|
||||||
|
trackEvent("link_click", {
|
||||||
|
link_url: linkUrl,
|
||||||
|
link_text: linkText || "unknown",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trackSearch = (searchTerm: string) => {
|
||||||
|
trackEvent("search", {
|
||||||
|
search_term: searchTerm,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Analytics, getAnalytics } from "firebase/analytics";
|
||||||
|
import { getApps, initializeApp } from "firebase/app";
|
||||||
|
|
||||||
|
const firebaseConfig = {
|
||||||
|
apiKey: "AIzaSyCTjdsk2jE34IfnvSKP1JMTIf_Abd7tbt0",
|
||||||
|
authDomain: "opplens-270d1.firebaseapp.com",
|
||||||
|
projectId: "opplens-270d1",
|
||||||
|
storageBucket: "opplens-270d1.firebasestorage.app",
|
||||||
|
messagingSenderId: "6923326255",
|
||||||
|
appId: "1:6923326255:web:cb4f630d92a286daed87da",
|
||||||
|
measurementId: "G-TMLH6BM963",
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize Firebase
|
||||||
|
const app =
|
||||||
|
getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
|
||||||
|
|
||||||
|
let analytics: Analytics | null = null;
|
||||||
|
|
||||||
|
// Initialize Analytics only on client side
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
analytics = getAnalytics(app);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { analytics, app };
|
||||||
Generated
+1026
-2
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,7 @@
|
|||||||
"@tanstack/react-query": "^5.90.7",
|
"@tanstack/react-query": "^5.90.7",
|
||||||
"apexcharts": "^5.3.5",
|
"apexcharts": "^5.3.5",
|
||||||
"axios": "^1.12.2",
|
"axios": "^1.12.2",
|
||||||
|
"firebase": "^12.6.0",
|
||||||
"next": "15.5.5",
|
"next": "15.5.5",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-apexcharts": "^1.8.0",
|
"react-apexcharts": "^1.8.0",
|
||||||
|
|||||||
Reference in New Issue
Block a user