- GDPR-Compliant Vercel Analytics Integration
GDPR-Compliant Vercel Analytics Integration
Learn how to extend your existing cookie consent system to conditionally load Vercel Analytics in a privacy-friendly and GDPR-compliant way.

Last month, I was feeling pretty good about myself. I'd just finished implementing a cookie consent system for a client's Next.js application. It came complete with granular controls, real-time updates across browser tabs, and Google Analytics integration that respected user preferences. The GDPR lawyers were happy, users had control, and everything seemed to be working perfectly.
Then the client asked: "Can we also add Vercel Analytics? We'd love to see those performance insights."
"Sure," I said confidently. "Just drop in the Analytics component and we're done."
Well, not so fast...
It turns out that while Google Analytics has built-in consent modes and extensive documentation for GDPR compliance, Vercel Analytics takes a more minimal approach. There's no built-in consent management – it just starts tracking immediately when loaded. This meant my carefully crafted consent system had a gaping hole: users who opted out of analytics cookies would still be tracked by Vercel.
If you've already implemented the cookie consent system from my previous guide, adding Vercel Analytics support is straightforward. Vercel Analytics doesn't have built-in consent management like Google Analytics, so we need to conditionally load it based on user preferences.
This extension will show you how to create a consent-aware wrapper for Vercel Analytics that respects your existing cookie preferences..
The Problem: When Privacy-Friendly Still Needs Consent
The issue wasn't immediately obvious because Vercel Analytics is genuinely privacy-friendly. It doesn't use persistent cookies, doesn't track users across sites, and anonymizes data by default. But "privacy-friendly" and "GDPR compliant" aren't the same thing.
Under GDPR, any form of user behavior tracking requires explicit consent – even if that tracking is anonymous and respectful. My existing consent system gave users granular control over different types of cookies:
- Essential: Always allowed (authentication, preferences)
- Analytics: Optional (Google Analytics, and now Vercel Analytics)
- Marketing: Optional (advertising, social media embeds)
The problem was in my layout file:
// app/layout.tsx - The problematic version
import { Analytics } from "@vercel/analytics/next";
import { GA4Consent } from '@/components/analytics/ga4-consent';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
<Analytics /> {/* This loads regardless of consent! */}
<GA4Consent gaId="G-XXXXXXXXX" />
</body>
</html>
);
}
While my Google Analytics integration waited for user consent through the GA4Consent
component, Vercel Analytics was loading immediately on every page. Users who specifically opted out of analytics tracking were still being tracked.
I needed a way to conditionally load Vercel Analytics based on the same consent preferences that controlled Google Analytics.
Exploring the Approach: Learning from What Already Worked
Before diving into implementation, I took a step back to understand what made my Google Analytics consent system work so well. The key was a consent-aware wrapper component that:
- Checked consent state before rendering any tracking scripts
- Listened for consent changes in real-time across browser tabs
- Handled server-side rendering properly to avoid hydration mismatches
- Integrated seamlessly with my existing cookie utilities
I had three options for Vercel Analytics:
Option 1: Load always, disable conditionally - Similar to how some Google Analytics implementations work, I could load Vercel Analytics and then try to disable tracking programmatically. This felt wrong because it still loads scripts unnecessarily and doesn't truly respect the "no consent" state.
Option 2: Server-side consent checking - I could check consent on the server and conditionally include the Analytics component. This would work for initial page loads but wouldn't handle real-time consent changes without page refreshes.
Option 3: Client-side consent wrapper - Create a component similar to my Google Analytics integration that only renders the Analytics component when consent is given.
I chose Option 3 because it matched my existing architecture and provided the best user experience. The user gets immediate feedback when changing consent preferences, and no unnecessary scripts are ever loaded without permission.
Step-by-Step Implementation: Building the Consent Wrapper
Step 1: Creating the Core Consent Component
The first step was creating a wrapper component that would conditionally render Vercel Analytics based on user consent. I started by examining my existing Google Analytics consent component to understand the pattern:
// components/analytics/vercel-analytics-consent.tsx
"use client";
import { useEffect, useState } from "react";
import { Analytics } from "@vercel/analytics/next";
import { shouldShowAnalytics } from "@/lib/client-cookie-utils";
export function VercelAnalyticsConsent() {
const [consentState, setConsentState] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setConsentState(shouldShowAnalytics());
setIsLoaded(true);
}, []);
useEffect(() => {
const handleConsentChange = () => {
setConsentState(shouldShowAnalytics());
};
window.addEventListener('storage', handleConsentChange);
window.addEventListener('consentUpdated', handleConsentChange);
return () => {
window.removeEventListener('storage', handleConsentChange);
window.removeEventListener('consentUpdated', handleConsentChange);
};
}, []);
// Only render Analytics component if consent is given and component is loaded
if (!isLoaded || !consentState) return null;
return <Analytics />;
}
This component follows the same pattern as my Google Analytics wrapper, but there are some key details worth explaining:
The shouldShowAnalytics()
function is part of my existing cookie utilities – it checks whether the user has consented to analytics cookies. By reusing this function, I ensure that both Google Analytics and Vercel Analytics respect the same user preferences.
The isLoaded
state prevents hydration mismatches between server and client. On the server, we can't access localStorage or cookies directly, so we assume no consent initially. Once the component mounts on the client, we check the actual consent state and update accordingly.
The event listeners handle real-time consent changes. When a user opens their cookie preferences in one browser tab and changes their analytics consent, all other open tabs immediately start or stop tracking. The storage
event fires when localStorage changes, and consentUpdated
is a custom event I dispatch when users interact with the consent banner.
💡 Tip: Always include cleanup in your event listeners to prevent memory leaks, especially in single-page applications where components may mount and unmount frequently.
Step 2: Updating the Layout Integration
With the consent wrapper created, I needed to replace the direct Vercel Analytics import in my layout:
// app/layout.tsx - Updated version
import { VercelAnalyticsConsent } from '@/components/analytics/vercel-analytics-consent';
import { GA4Consent } from '@/components/analytics/ga4-consent';
import { BannerController } from '@/components/cookie-consent/banner-controller';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
{/* Cookie consent banner */}
<BannerController />
{/* Analytics - only load with consent */}
<VercelAnalyticsConsent />
<GA4Consent gaId="G-XXXXXXXXX" />
</body>
</html>
);
}
This change is deceptively simple but represents a fundamental shift in how analytics loading works. Instead of unconditionally loading Vercel Analytics, we're now using a consent-aware component that respects user preferences.
The placement is important too. I put the analytics components after the BannerController
so that the consent banner can render first and handle any initial consent decisions before analytics components try to load.
Step 3: Adding Advanced Configuration Options
After testing the basic implementation, I realized there were some edge cases I wanted to handle. What if I needed to filter certain events or add additional privacy controls? I extended the wrapper to support Vercel Analytics' configuration options:
// components/analytics/vercel-analytics-consent.tsx - Enhanced version
"use client";
import { useEffect, useState } from "react";
import { Analytics, type BeforeSendEvent } from "@vercel/analytics/next";
import { shouldShowAnalytics } from "@/lib/client-cookie-utils";
export function VercelAnalyticsConsent() {
const [consentState, setConsentState] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setConsentState(shouldShowAnalytics());
setIsLoaded(true);
}, []);
useEffect(() => {
const handleConsentChange = () => {
setConsentState(shouldShowAnalytics());
};
window.addEventListener('storage', handleConsentChange);
window.addEventListener('consentUpdated', handleConsentChange);
return () => {
window.removeEventListener('storage', handleConsentChange);
window.removeEventListener('consentUpdated', handleConsentChange);
};
}, []);
const handleBeforeSend = (event: BeforeSendEvent) => {
// Double-check consent before sending any data
if (!shouldShowAnalytics()) {
return null; // Block the event
}
// Filter out sensitive URLs
if (event.url.includes('/admin') || event.url.includes('/private')) {
return null;
}
return event;
};
if (!isLoaded || !consentState) return null;
return (
<Analytics
beforeSend={handleBeforeSend}
debug={process.env.NODE_ENV === 'development'}
/>
);
}
The beforeSend
callback adds an extra layer of protection. Even if there's a bug in my consent logic, this function ensures no data is sent to Vercel Analytics without proper consent. It also allows me to filter out sensitive URLs that should never be tracked, regardless of consent status.
The debug mode is helpful during development to see exactly when analytics events are being sent or blocked.
⚠️ Common Bug: Don't forget that the beforeSend
callback runs for every analytics event, so keep the logic lightweight to avoid performance issues.
Debugging & Common Pitfalls: What I Learned the Hard Way
The Hydration Mismatch Mystery
My first implementation had a subtle bug that only appeared intermittently. Sometimes the Vercel Analytics component would flash briefly before disappearing, and I'd see hydration warnings in the console.
The issue was that I was checking consent immediately during component initialization:
// Problematic version - don't do this
export function VercelAnalyticsConsent() {
const [consentState, setConsentState] = useState(shouldShowAnalytics()); // Problem!
// ... rest of component
}
The problem is that shouldShowAnalytics()
accesses localStorage, which doesn't exist during server-side rendering. The server would always assume false
, but if the client had previously given consent, the initial client render would be true
, causing a mismatch.
The solution was to always start with false
and then check consent after the component mounts:
// Fixed version
export function VercelAnalyticsConsent() {
const [consentState, setConsentState] = useState(false); // Always start false
const [isLoaded, setIsLoaded] = useState(false);
useEffect(() => {
setConsentState(shouldShowAnalytics()); // Check after mount
setIsLoaded(true);
}, []);
// ... rest of component
}
The Cross-Tab Synchronization Challenge
Another issue I discovered during testing was that consent changes weren't always propagating across browser tabs immediately. A user might deny consent in one tab, but Vercel Analytics would continue tracking in other open tabs.
This happened because I was only listening for localStorage
changes, but some of my consent update logic was using sessionStorage or directly updating state without triggering storage events.
I solved this by standardizing on a custom event system and ensuring all consent changes dispatch a consentUpdated
event:
// In my cookie utilities
export function updateCookieConsent(preferences: CookiePreferences) {
// Update localStorage
localStorage.setItem('cookie-consent', JSON.stringify(preferences));
// Dispatch custom event for immediate cross-tab updates
window.dispatchEvent(new CustomEvent('consentUpdated', {
detail: preferences
}));
}
This ensures that any consent change immediately propagates to all open tabs, regardless of how the change was triggered.
The Network Request Timing Issue
During testing, I noticed that sometimes Vercel Analytics would briefly start making network requests before being disabled when a user revoked consent. This happened because there's a small delay between the consent change event and the component re-rendering.
The beforeSend
callback solved this by adding a final consent check right before any data is sent:
const handleBeforeSend = (event: BeforeSendEvent) => {
// This runs right before sending, catching any race conditions
if (!shouldShowAnalytics()) {
return null;
}
return event;
};
This extra safety net ensures that even if there are timing issues in the React rendering cycle, no data gets sent without proper consent.
Final Working Version: Putting It All Together
Here's the complete implementation that handles all the edge cases I discovered:
// components/analytics/vercel-analytics-consent.tsx - Final version
"use client";
import { useEffect, useState } from "react";
import { Analytics, type BeforeSendEvent } from "@vercel/analytics/next";
import { shouldShowAnalytics } from "@/lib/client-cookie-utils";
export function VercelAnalyticsConsent() {
const [consentState, setConsentState] = useState(false);
const [isLoaded, setIsLoaded] = useState(false);
// Check initial consent state after component mounts
useEffect(() => {
setConsentState(shouldShowAnalytics());
setIsLoaded(true);
}, []);
// Listen for consent changes across all browser tabs
useEffect(() => {
const handleConsentChange = () => {
setConsentState(shouldShowAnalytics());
};
window.addEventListener('storage', handleConsentChange);
window.addEventListener('consentUpdated', handleConsentChange);
return () => {
window.removeEventListener('storage', handleConsentChange);
window.removeEventListener('consentUpdated', handleConsentChange);
};
}, []);
// Final consent check before sending any data
const handleBeforeSend = (event: BeforeSendEvent) => {
if (!shouldShowAnalytics()) {
return null;
}
// Optional: Filter sensitive URLs
if (event.url.includes('/admin') || event.url.includes('/private')) {
return null;
}
return event;
};
// Don't render until loaded and consent is given
if (!isLoaded || !consentState) return null;
return (
<Analytics
beforeSend={handleBeforeSend}
debug={process.env.NODE_ENV === 'development'}
/>
);
}
And the updated layout that ties everything together:
// app/layout.tsx - Complete integration
import { VercelAnalyticsConsent } from '@/components/analytics/vercel-analytics-consent';
import { GA4Consent } from '@/components/analytics/ga4-consent';
import { BannerController } from '@/components/cookie-consent/banner-controller';
export default function RootLayout({ children }) {
return (
<html lang="en">
<body>
{children}
{/* Cookie consent banner */}
<BannerController />
{/* Analytics - only load with proper consent */}
<VercelAnalyticsConsent />
<GA4Consent gaId="G-XXXXXXXXX" />
</body>
</html>
);
}
The beauty of this solution is its simplicity. From the outside, it looks like a straightforward component swap, but under the hood, it provides comprehensive consent management with real-time updates, cross-tab synchronization, and multiple layers of privacy protection.
Testing Your Implementation: Verification Steps
To make sure everything works correctly, I developed a testing checklist that covers all the edge cases:
-
Initial state test: Clear all cookies and visit your site. Verify that no Vercel Analytics network requests are made in the browser's network tab.
-
Consent flow test: Accept analytics cookies through your consent banner and confirm that Vercel Analytics starts making requests immediately without requiring a page reload.
-
Real-time updates test: With analytics enabled, open your cookie preferences and disable analytics. Verify that tracking stops immediately in the same tab.
-
Cross-tab synchronization test: Open your site in multiple browser tabs. Change consent preferences in one tab and verify that all other tabs start or stop tracking immediately.
-
Page navigation test: With analytics enabled, navigate between pages and confirm that page views are being tracked properly.
-
Sensitive URL filtering test: If you implemented URL filtering, visit admin or private pages and verify they're not being tracked even with consent.
The network tab in your browser's developer tools is your best friend for this testing. You should see vercel analytics requests starting and stopping in real-time as consent changes.
Optional Improvements & Ideas: Where to Go Next
Now that you have a solid foundation, there are several ways you could extend this implementation:
Enhanced Privacy Controls: You could add more granular controls, allowing users to consent to performance analytics separately from behavior analytics, then configure Vercel Analytics accordingly.
Custom Event Tracking: With the consent system in place, you could add custom event tracking that also respects user preferences:
// utils/analytics.ts
export function trackEvent(eventName: string, properties?: object) {
if (!shouldShowAnalytics()) return;
// Your custom event tracking logic here
}
Analytics Dashboard Integration: You could build an admin dashboard that shows analytics data only for users who have consented, providing complete transparency about data collection.
Consent Analytics: Ironically, you might want to track consent preferences themselves (with appropriate consent, of course) to understand how users interact with your privacy controls.
Multi-Region Compliance: If you're operating in multiple jurisdictions, you could extend the system to handle different privacy laws automatically based on user location.
The key insight I gained from this implementation is that privacy compliance doesn't have to be an afterthought. By building consent management into the foundation of your analytics setup, you create a system that's both respectful of user privacy and easy to extend with new tracking tools.
✅ Best Practice: Always test your consent system with a fresh browser session. It's easy to get false positives when you're testing with cookies already set from previous development work.
The most rewarding part of this project was realizing that the consent-first approach actually improved the user experience. Users get immediate feedback when they change their preferences, analytics only loads when it's wanted, and the whole system feels more trustworthy as a result.
Privacy compliance isn't just about avoiding legal issues – it's about building trust with your users. And trust, as any developer knows, is the foundation of everything we build.