In the world of web applications, understanding user behavior and measuring effectiveness is key to making sound business and technical decisions. Integrating analytics tools with a Next.js application allows tracking traffic, user behavior, conversions, and other important performance indicators.
The most commonly used analytics tools in Next.js applications include:
Google Analytics is the most popular analytics tool. Here's how to integrate GA4 with a Next.js application:
Start by creating a Google Analytics account and configuring a data stream:
Create a Google Analytics component:
1// components/GoogleAnalytics.tsx
2'use client';
3
4import Script from 'next/script';
5
6export default function GoogleAnalytics({ GA_MEASUREMENT_ID }: { GA_MEASUREMENT_ID: string }) {
7 return (
8 <>
9 <Script
10 strategy="afterInteractive"
11 src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
12 />
13 <Script
14 id="google-analytics"
15 strategy="afterInteractive"
16 dangerouslySetInnerHTML={{
17 __html: `
18 window.dataLayer = window.dataLayer || [];
19 function gtag(){dataLayer.push(arguments);}
20 gtag('js', new Date());
21 gtag('config', '${GA_MEASUREMENT_ID}', {
22 page_path: window.location.pathname,
23 cookie_flags: 'SameSite=None;Secure'
24 });
25 `,
26 }}
27 />
28 </>
29 );
30}Then add the component to the main application layout:
1// app/layout.tsx
2import GoogleAnalytics from '@/components/GoogleAnalytics';
3
4export default function RootLayout({
5 children,
6}: {
7 children: React.ReactNode;
8}) {
9 return (
10 <html lang="pl">
11 <body>
12 {process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID && (
13 <GoogleAnalytics GA_MEASUREMENT_ID={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID} />
14 )}
15 {children}
16 </body>
17 </html>
18 );
19}Make sure to add the environment variable to the file
.env.local:1NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXXIn App Router, you should implement URL change tracking because client-side navigation does not cause a full page refresh:
1// hooks/usePageTracking.ts
2'use client';
3
4import { usePathname, useSearchParams } from 'next/navigation';
5import { useEffect } from 'react';
6
7export function usePageTracking() {
8 const pathname = usePathname();
9 const searchParams = useSearchParams();
10
11 useEffect(() => {
12 if (pathname && window.gtag) {
13 let url = window.origin + pathname;
14 if (searchParams.toString()) {
15 url = url + '?' + searchParams.toString();
16 }
17
18 window.gtag('config', process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!, {
19 page_path: pathname,
20 page_location: url,
21 });
22 }
23 }, [pathname, searchParams]);
24}Then use this hook in a client component:
1// components/PageTracker.tsx
2'use client';
3
4import { usePageTracking } from '@/hooks/usePageTracking';
5
6export default function PageTracker() {
7 usePageTracking();
8 return null;
9}
10
11// app/layout.tsx
12import PageTracker from '@/components/PageTracker';
13
14export default function RootLayout({ children }: { children: React.ReactNode }) {
15 return (
16 <html lang="pl">
17 <body>
18 {process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID && (
19 <>
20 <GoogleAnalytics GA_MEASUREMENT_ID={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID} />
21 <PageTracker />
22 </>
23 )}
24 {children}
25 </body>
26 </html>
27 );
28}Create a tool for tracking custom events:
1// lib/analytics.ts
2'use client';
3
4export type EventOptions = {
5 category: string;
6 label?: string;
7 value?: number;
8 nonInteraction?: boolean;
9 [key: string]: any;
10};
11
12export const trackEvent = (action: string, options: EventOptions) => {
13 const { category, label, value, nonInteraction = false, ...restOptions } = options;
14
15 if (typeof window !== 'undefined' && window.gtag) {
16 window.gtag('event', action, {
17 event_category: category,
18 event_label: label,
19 value: value,
20 non_interaction: nonInteraction,
21 ...restOptions,
22 });
23 }
24};Usage in a component:
1// components/SubscribeButton.tsx
2'use client';
3
4import { trackEvent } from '@/lib/analytics';
5
6export default function SubscribeButton() {
7 const handleSubscribe = () => {
8 // Subscription logic...
9
10 // Event tracking
11 trackEvent('subscribe', {
12 category: 'Engagement',
13 label: 'Newsletter',
14 value: 1,
15 });
16 };
17
18 return (
19 <button
20 onClick={handleSubscribe}
21 className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"
22 >
23 Subscribe to newsletter
24 </button>
25 );
26}If you host your Next.js application on Vercel, you can take advantage of Vercel's built-in analytics, which offers easy deployment and solid performance metrics.
1npm install @vercel/analytics
2# or
3yarn add @vercel/analytics
4# or
5pnpm add @vercel/analytics1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3
4export default function RootLayout({
5 children,
6}: {
7 children: React.ReactNode;
8}) {
9 return (
10 <html lang="pl">
11 <body>
12 {children}
13 <Analytics />
14 </body>
15 </html>
16 );
17}That's all! Vercel automatically collects data on page visits and key Web Vitals metrics.
1// components/ContactForm.tsx
2'use client';
3
4import { track } from '@vercel/analytics';
5import { useState } from 'react';
6
7export default function ContactForm() {
8 const [email, setEmail] = useState('');
9
10 const handleSubmit = (e: React.FormEvent) => {
11 e.preventDefault();
12 // Contact form logic...
13
14 // Event tracking
15 track('contact_form_submitted', { email_domain: email.split('@')[1] });
16 };
17
18 return (
19 <form onSubmit={handleSubmit} className="space-y-4">
20 <div>
21 <label htmlFor="email" className="block mb-1">Email</label>
22 <input
23 id="email"
24 type="email"
25 value={email}
26 onChange={(e) => setEmail(e.target.value)}
27 required
28 className="w-full px-3 py-2 border rounded"
29 />
30 </div>
31 <button
32 type="submit"
33 className="bg-blue-500 hover:bg-blue-600 text-white py-2 px-4 rounded"
34 >
35 Send
36 </button>
37 </form>
38 );
39}With growing privacy awareness and regulations such as GDPR, many companies choose privacy-friendly analytics tools. Below you will find an example of integration with Plausible Analytics.
1// components/PlausibleAnalytics.tsx
2'use client';
3
4import Script from 'next/script';
5
6export default function PlausibleAnalytics({ domain }: { domain: string }) {
7 return (
8 <Script
9 strategy="afterInteractive"
10 data-domain={domain}
11 src="https://plausible.io/js/script.js"
12 />
13 );
14}
15
16// app/layout.tsx
17import PlausibleAnalytics from '@/components/PlausibleAnalytics';
18
19export default function RootLayout({
20 children,
21}: {
22 children: React.ReactNode;
23}) {
24 return (
25 <html lang="pl">
26 <body>
27 {process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN && (
28 <PlausibleAnalytics domain={process.env.NEXT_PUBLIC_ANALYTICS_DOMAIN} />
29 )}
30 {children}
31 </body>
32 </html>
33 );
34}PostHog is a comprehensive open-source analytics platform that combines analytics, session recording, AB testing features, and more.
1// app/providers.tsx
2'use client';
3
4import posthog from 'posthog-js';
5import { PostHogProvider } from 'posthog-js/react';
6import { usePathname, useSearchParams } from 'next/navigation';
7import { useEffect } from 'react';
8
9if (typeof window !== 'undefined' && process.env.NEXT_PUBLIC_POSTHOG_KEY) {
10 posthog.init(process.env.NEXT_PUBLIC_POSTHOG_KEY, {
11 api_host: process.env.NEXT_PUBLIC_POSTHOG_HOST || 'https://app.posthog.com',
12 // Other options...
13 capture_pageview: false // Disable automatic page view tracking
14 });
15}
16
17export function PostHogPageview(): JSX.Element {
18 const pathname = usePathname();
19 const searchParams = useSearchParams();
20
21 useEffect(() => {
22 if (pathname) {
23 let url = window.origin + pathname;
24 if (searchParams && searchParams.toString()) {
25 url = url + `?${searchParams.toString()}`;
26 }
27 posthog.capture('$pageview', {
28 $current_url: url,
29 });
30 }
31 }, [pathname, searchParams]);
32
33 return <></>;
34}
35
36export function PHProvider({ children }: { children: React.ReactNode }) {
37 return (
38 <PostHogProvider client={posthog}>
39 <PostHogPageview />
40 {children}
41 </PostHogProvider>
42 );
43}
44
45// app/layout.tsx
46import { PHProvider } from './providers';
47
48export default function RootLayout({
49 children,
50}: {
51 children: React.ReactNode;
52}) {
53 return (
54 <html lang="pl">
55 <body>
56 <PHProvider>{children}</PHProvider>
57 </body>
58 </html>
59 );
60}In accordance with personal data protection regulations, user consent must be obtained before starting tracking. Implementation of a simple cookie banner:
1// components/CookieConsent.tsx
2'use client';
3
4import { useState, useEffect } from 'react';
5
6export default function CookieConsent() {
7 const [consent, setConsent] = useState<boolean | null>(null);
8
9 useEffect(() => {
10 // Check if the user has already given consent
11 const storedConsent = localStorage.getItem('cookieConsent');
12 if (storedConsent !== null) {
13 setConsent(storedConsent === 'true');
14 } else {
15 setConsent(false); // No consent by default
16 }
17 }, []);
18
19 const handleAccept = () => {
20 localStorage.setItem('cookieConsent', 'true');
21 setConsent(true);
22
23 // Enable analytics
24 if (window.gtag) {
25 window.gtag('consent', 'update', {
26 'analytics_storage': 'granted'
27 });
28 }
29 };
30
31 const handleReject = () => {
32 localStorage.setItem('cookieConsent', 'false');
33 setConsent(false);
34
35 // Disable analytics
36 if (window.gtag) {
37 window.gtag('consent', 'update', {
38 'analytics_storage': 'denied'
39 });
40 }
41 };
42
43 if (consent === null || consent === true) {
44 return null; // Don't show banner if consent has been given or hasn't loaded yet
45 }
46
47 return (
48 <div className="fixed bottom-0 left-0 right-0 bg-gray-100 p-4 shadow-md z-50">
49 <div className="max-w-6xl mx-auto flex flex-col md:flex-row items-center justify-between">
50 <div className="mb-4 md:mb-0">
51 <p className="text-sm">
52 This page uses cookies to analyze traffic. You can decide whether to accept cookies.
53 </p>
54 </div>
55 <div className="flex space-x-4">
56 <button
57 onClick={handleReject}
58 className="px-4 py-2 text-sm border border-gray-300 rounded hover:bg-gray-200"
59 >
60 Reject
61 </button>
62 <button
63 onClick={handleAccept}
64 className="px-4 py-2 text-sm bg-blue-500 text-white rounded hover:bg-blue-600"
65 >
66 Accept
67 </button>
68 </div>
69 </div>
70 </div>
71 );
72}
73
74// app/layout.tsx
75import CookieConsent from '@/components/CookieConsent';
76
77export default function RootLayout({
78 children,
79}: {
80 children: React.ReactNode;
81}) {
82 return (
83 <html lang="pl">
84 <body>
85 {children}
86 <CookieConsent />
87 </body>
88 </html>
89 );
90}Modify the Google Analytics initialization to take user consent into account:
1// components/GoogleAnalytics.tsx
2'use client';
3
4import Script from 'next/script';
5import { useEffect } from 'react';
6
7export default function GoogleAnalytics({ GA_MEASUREMENT_ID }: { GA_MEASUREMENT_ID: string }) {
8 useEffect(() => {
9 // Check the user's consent
10 const consent = localStorage.getItem('cookieConsent') === 'true';
11
12 // Update the consent state in Google Analytics
13 if (window.gtag) {
14 window.gtag('consent', 'update', {
15 'analytics_storage': consent ? 'granted' : 'denied'
16 });
17 }
18 }, []);
19
20 return (
21 <>
22 <Script
23 strategy="afterInteractive"
24 src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
25 />
26 <Script
27 id="google-analytics"
28 strategy="afterInteractive"
29 dangerouslySetInnerHTML={{
30 __html: `
31 window.dataLayer = window.dataLayer || [];
32 function gtag(){dataLayer.push(arguments);}
33 gtag('js', new Date());
34
35 // Set default consent as 'denied'
36 gtag('consent', 'default', {
37 'analytics_storage': 'denied'
38 });
39
40 // Configure Google Analytics
41 gtag('config', '${GA_MEASUREMENT_ID}', {
42 page_path: window.location.pathname,
43 });
44 `,
45 }}
46 />
47 </>
48 );
49}For applications that run on multiple platforms, it's worth implementing consistent analytics tracking. You can use libraries such as Segment, Amplitude or Firebase Analytics, which offer SDKs for different platforms.
1// lib/analytics.ts
2import { initializeApp } from 'firebase/app';
3import { getAnalytics, logEvent, isSupported } from 'firebase/analytics';
4
5const firebaseConfig = {
6 apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
7 authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
8 projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
9 // Other configuration...
10};
11
12let analytics: any = null;
13
14export async function initAnalytics() {
15 if (typeof window !== 'undefined' && await isSupported()) {
16 const app = initializeApp(firebaseConfig);
17 analytics = getAnalytics(app);
18 return true;
19 }
20 return false;
21}
22
23export function trackEvent(eventName: string, eventParams?: any) {
24 if (analytics) {
25 logEvent(analytics, eventName, eventParams);
26 }
27}
28
29// components/AnalyticsProvider.tsx
30'use client';
31
32import { useEffect } from 'react';
33import { usePathname, useSearchParams } from 'next/navigation';
34import { initAnalytics, trackEvent } from '@/lib/analytics';
35
36export default function AnalyticsProvider({ children }: { children: React.ReactNode }) {
37 const pathname = usePathname();
38 const searchParams = useSearchParams();
39
40 useEffect(() => {
41 initAnalytics();
42 }, []);
43
44 useEffect(() => {
45 if (pathname) {
46 trackEvent('page_view', {
47 page_path: pathname,
48 page_search: searchParams?.toString(),
49 });
50 }
51 }, [pathname, searchParams]);
52
53 return <>{children}</>;
54}Web Vitals metrics are key to understanding your application's performance. Next.js offers a built-in tool for reporting Web Vitals to analytics systems:
1// app/layout.tsx
2import { useReportWebVitals } from 'next/web-vitals';
3
4export function WebVitals() {
5 useReportWebVitals((metric) => {
6 const { id, name, label, value } = metric;
7
8 // Reporting to Google Analytics
9 if (window.gtag) {
10 window.gtag('event', name, {
11 event_category: 'Web Vitals',
12 event_label: label,
13 value: Math.round(name === 'CLS' ? value * 1000 : value),
14 non_interaction: true,
15 metric_id: id,
16 });
17 }
18
19 // Reporting to the console in the development environment
20 if (process.env.NODE_ENV === 'development') {
21 console.log(`Web Vital: ${name}`, metric);
22 }
23 });
24
25 return null;
26}
27
28export default function RootLayout({ children }: { children: React.ReactNode }) {
29 return (
30 <html lang="pl">
31 <body>
32 <WebVitals />
33 {children}
34 </body>
35 </html>
36 );
37}Sometimes you need to visualize analytics data directly in your application. You can implement your own analytics dashboard by fetching data from the Google Analytics API or another tool:
1// app/admin/analytics/page.tsx
2import { google } from 'googleapis';
3import { BarChart, LineChart } from '@/components/Charts';
4
5async function getAnalyticsData() {
6 try {
7 // Initialize the Google Analytics client
8 const auth = new google.auth.GoogleAuth({
9 keyFile: process.env.GOOGLE_APPLICATION_CREDENTIALS,
10 scopes: ['https://www.googleapis.com/auth/analytics.readonly'],
11 });
12
13 const analyticsreporting = google.analyticsreporting({
14 version: 'v4',
15 auth,
16 });
17
18 // Fetch the analytics data
19 const response = await analyticsreporting.reports.batchGet({
20 requestBody: {
21 reportRequests: [
22 {
23 viewId: process.env.GA_VIEW_ID,
24 dateRanges: [{ startDate: '30daysAgo', endDate: 'today' }],
25 metrics: [{ expression: 'ga:sessions' }, { expression: 'ga:pageviews' }],
26 dimensions: [{ name: 'ga:date' }],
27 },
28 ],
29 },
30 });
31
32 // Process the data
33 const report = response.data.reports[0];
34 const rows = report.data.rows;
35
36 return rows.map((row) => ({
37 date: row.dimensions[0],
38 sessions: parseInt(row.metrics[0].values[0], 10),
39 pageviews: parseInt(row.metrics[0].values[1], 10),
40 }));
41 } catch (error) {
42 console.error('Error during fetching analytics data:', error);
43 return [];
44 }
45}
46
47export default async function AnalyticsDashboard() {
48 const analyticsData = await getAnalyticsData();
49
50 return (
51 <div className="p-6">
52 <h1 className="text-2xl font-bold mb-6">Analytics dashboard</h1>
53
54 <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
55 <div className="bg-white p-4 rounded shadow">
56 <h2 className="text-lg font-semibold mb-4">Sessions vs. page views</h2>
57 <LineChart
58 data={analyticsData}
59 categories={['sessions', 'pageviews']}
60 xKey="date"
61 />
62 </div>
63
64 <div className="bg-white p-4 rounded shadow">
65 <h2 className="text-lg font-semibold mb-4">Page views by day</h2>
66 <BarChart
67 data={analyticsData}
68 category="pageviews"
69 xKey="date"
70 />
71 </div>
72 </div>
73 </div>
74 );
75}Before deploying to production, it's worth testing the analytics integration in the development environment:
1// lib/analytics.ts
2export function trackEvent(eventName: string, eventParams: any = {}) {
3 // In the development environment, print events to the console
4 if (process.env.NODE_ENV === 'development') {
5 console.log(`Analytics Event: ${eventName}`, eventParams);
6 return;
7 }
8
9 // In the production environment, send to the analytics service
10 if (window.gtag) {
11 window.gtag('event', eventName, eventParams);
12 }
13}For testing you can also use browser extensions such as Google Analytics Debugger, Tag Assistant or Facebook Pixel Helper.
afterInteractive or lazyOnload for analytics scriptsFor more advanced needs, it's worth considering using specialized analytics frameworks for Next.js:
Web analytics is a key tool for understanding users and optimizing applications. Next.js offers many ways to integrate with various analytics tools, from Google Analytics through Vercel Analytics to more privacy-friendly alternatives such as Plausible or Fathom.
Remember that the most important thing is to collect only the data that is really needed and to do it with respect for user privacy. Good practices include obtaining consent, anonymizing data, and informing users about what data is collected and for what purpose.