Module 6
GA4 Measurement Protocol: server-side event from a Next.js route handler
Send a GA4 event directly from a Next.js route handler using the Measurement Protocol. Sessions stitch correctly when client_id is read from the _ga cookie.
What this does
The GA4 Measurement Protocol lets you send events directly from your server to Google Analytics, bypassing the browser entirely. This is useful for tracking server-side conversions (lead form submissions, payment confirmations) where the browser event may be blocked or unreliable.
The key implementation detail: read the _ga cookie from the request to get client_id, which stitches the server-side hit to the existing browser session.
The route handler
// app/api/track/route.ts
import { cookies } from 'next/headers';
const GA4_MEASUREMENT_ID = process.env.GA4_MEASUREMENT_ID;
const GA4_MP_API_SECRET = process.env.GA4_MP_API_SECRET;
/**
* Extract the GA4 client_id from the _ga cookie.
* The _ga cookie format is: GA1.1.{client_id_part1}.{client_id_part2}
* client_id = part1 + '.' + part2
*/
function extractClientId(gaCookie: string | undefined): string {
if (!gaCookie) return 'unknown';
const parts = gaCookie.split('.');
if (parts.length >= 4) {
return `${parts[2]}.${parts[3]}`;
}
return gaCookie;
}
export async function POST(request: Request) {
// Guard: no-op when env is absent
if (!GA4_MEASUREMENT_ID || !GA4_MP_API_SECRET) {
return Response.json({ ok: true, sent: false });
}
const cookieStore = await cookies();
const clientId = extractClientId(cookieStore.get('_ga')?.value);
const body = (await request.json()) as { eventName: string; params?: Record<string, unknown> };
const payload = {
client_id: clientId,
events: [
{
name: body.eventName,
params: {
engagement_time_msec: 100,
session_id: Date.now().toString(),
...body.params,
},
},
],
};
try {
const res = await fetch(
`https://www.google-analytics.com/mp/collect` +
`?measurement_id=${GA4_MEASUREMENT_ID}` +
`&api_secret=${GA4_MP_API_SECRET}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload),
}
);
return Response.json({ ok: true, sent: true, status: res.status });
} catch (err) {
console.error('[ga4-mp] send failed:', err);
return Response.json({ ok: true, sent: false }, { status: 500 });
}
}Required environment variables
# .env.local
GA4_MEASUREMENT_ID=G-XXXXXXXXXX
GA4_MP_API_SECRET=your_api_secret_hereThe API secret is created in GA4 Admin: Data Streams → your stream → Measurement Protocol API secrets.
Verifying in GA4 DebugView
Add ?measurement_debug_mode=1 to the payload's params to route hits to DebugView:
params: {
...body.params,
debug_mode: true, // Routes to GA4 DebugView
}DebugView shows the event in real time within 1–2 seconds of the server send.
Consent guard
Never send Measurement Protocol events without verifying consent first. Read the consent cookie server-side and check before sending:
// Only send when analytics_storage is granted
const consentCookie = cookieStore.get('al_consent')?.value;
const params = new URLSearchParams(decodeURIComponent(consentCookie ?? ''));
const analyticsConsented = params.get('a') === '1';
if (!analyticsConsented) {
return Response.json({ ok: true, sent: false, reason: 'consent_denied' });
}The free Server-Side GTM Base Container shows how to receive these hits through sGTM instead of sending directly.
Free container for this module
Server-Side GTM Base Container
A production-ready server-side GTM container configuration. Includes a GA4 client, GA4 event tag, and the routing setup needed to receive web container hits at your custom domain.
Get the container →