Send GA4 Events From Browser and Server

Send GA4 click events from a Next.js browser and server, then compare delivery.

Introduction

30 Second Summary

A button click can vanish from your reports when a browser blocks tracking. That leaves you making decisions from a conversion count that looks complete.

In this project, you will build a tracked button in a Next.js app. Each click reaches Google Analytics 4 through a browser tag plus a server route that uses the Measurement Protocol.

What You'll Build

You'll click one button with an ad blocker enabled and watch the Realtime report reveal which tracking path still records the conversion.

By the end of this project, you'll have:

  • Trigger a single click and see a named browser event appear in the Realtime report.
  • Send a server-side event through your own API route. Read Google's validation messages for the payload.
  • Enable an ad blocker and perform a side-by-side event comparison to see which tracking path still records the click.
  • Secret Mission: take on an optional challenge to push your tracking skills further.

Are there any prerequisites?

You need a Google Analytics property plus a Webflow site. You also need Node plus an existing Next.js app.

Before We Start

Your dual-tracking experiment needs a local app that can run browser code. The same app must also run server code.

Next.js gives you both layers in one project. Google Analytics 4 gives you the property where your events appear.

In this step, get ready to:
  • Confirm that your local JavaScript runtime meets the Next.js requirement.
  • Create a runnable Next.js App Router project.
  • Confirm access to the Admin view and Realtime report for a Google Analytics 4 property.
Check Node.js and npm

Node.js runs your Next.js development tools. Next.js requires Node.js 20.9 or higher.

The Node.js installer also includes npm. You use npm to create the project and start its development server.

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Terminal on macOS or Windows Terminal on Windows.
  • Press Enter to open the terminal.
  • Check the installed Node.js and npm versions by running:
node --version
npm --version

What do these checks prove?

  • The first line confirms that Node.js is installed and shows its version.
  • The second line confirms that npm is available for creating your app.

✔️ I see a supported version

Your Node.js version is 20.9 or higher. npm also prints a version number.

  • Continue to the project creation substep below.

ⓧ I see an older version

Your installed Node.js version is below 20.9. Updating it gives Next.js the runtime features it expects.

  • Visit the official Node.js download page.
  • Download the LTS installer for your operating system.
  • Run the downloaded installer.
  • Follow the installer prompts to replace the older version.
  • Close your current terminal after the installation finishes.
  • Open a fresh terminal through your operating system search.
  • Repeat the two version checks above.

Continue when Node.js reports 20.9 or higher and npm prints a version number.

ⓧ Command not found

Node.js is not installed yet. The official installer adds Node.js and npm together.

  • Visit the official Node.js download page.
  • Download the LTS installer for your operating system.
  • Run the downloaded installer.
  • Follow the installer prompts to complete the installation.
  • Close your current terminal after the installation finishes.
  • Open a fresh terminal through your operating system search.
  • Repeat the two version checks above.

Continue when Node.js reports 20.9 or higher and npm prints a version number.

Still missing Node.js or npm?

A terminal opened before installation may not detect the new commands. Close every terminal window before opening a fresh one.

If the problem continues, restart your computer so the updated command path loads.

Help me troubleshoot my Node.js installation.

Create and run the Next.js app

The App Router keeps browser pages and server routes in the same application. That shared structure supports both halves of your tracking experiment.

  • Move to your Desktop by running this command:
cd ~/Desktop

What does this command do?

The cd command changes the terminal location. Your new project folder appears on your Desktop where it is easy to find.

The scaffold downloads the framework and installs its dependencies. This can take a few minutes on the first run.

  • Approve the download if npm asks to fetch the scaffolding package.
  • Create the Next.js project with the recommended defaults by running:
npx create-next-app@latest my-app --yes

What does this command create?

  • The command creates a my-app folder on your Desktop.
  • The recommended defaults enable TypeScript and the App Router.
  • The --yes option skips the configuration questions by accepting those defaults.

Before you start the server, what do you expect to see when the generated app loads in your browser?

  • Enter the new project folder and start its development server by running:
cd my-app
npm run dev

What happens when the server starts?

The first command moves the terminal into my-app. The second command starts the Next.js development server.

The server keeps control of this terminal while it runs. Press Ctrl+C when you eventually want to stop it.

  • Open http://localhost:3000 in your browser.

You should see the generated Next.js starter page. That visible page proves your App Router project can run locally.

App not loading?

Keep the terminal running while you visit the local address. Stopping the command also stops the development server.

If the scaffold failed, check the terminal for a network or package installation problem before running the creation command again.

Help me fix my local Next.js app.

Access a Google Analytics 4 property

Your later browser and server events need a destination. A Google Analytics 4 property provides the Admin settings and Realtime report used throughout the experiment.

✔️ I already have a GA4 property

Use an existing property when you can view its Admin settings and Realtime report.

  • Open Google Analytics in your browser.
  • Sign in to your Google Account if prompted.
  • Click the property selector in the top-left corner.
  • Select a GA4 property that you can use for this project.
  • Click Admin in the left navigation.
  • Confirm that the selected property name appears in the property area.
  • Select Reports in the left navigation.
  • Click Realtime.

You should be able to move between the property settings and its Realtime report.

ⓧ I need to create a property

Your existing Analytics account can hold a new property for this experiment. You need Editor access to create it.

  • Open Google Analytics in your browser.
  • Sign in to your Google Account.
  • Click Admin in the left navigation.
  • Click Create.
  • Select Property.
  • Enter GA4 Click Tracker as the property name.
  • Select your reporting time zone.
  • Select your reporting currency.

Why do these property settings matter?

The reporting time zone controls which date receives each event. The currency setting controls how monetary metrics appear.

  • Click Next.
  • Select the industry category that best fits your work.
  • Select your business size.
  • Click Next.
  • Select how you intend to use Google Analytics.
  • Click Create.
  • Accept the displayed terms if Google asks you to.

That setup hurdle is cleared. Your new property is ready for the tracking configuration in the next step.

  • Click Admin in the left navigation.
  • Confirm that GA4 Click Tracker appears as the selected property.
  • Select Reports in the left navigation.
  • Click Realtime.

You should see the Realtime report for your new property.

ⓧ This is my first Analytics property

A new Analytics account gives your first property somewhere to live. The setup flow creates the account before moving directly into property creation.

  • Open Google Analytics in your browser.
  • Sign in to your Google Account.
  • Click Start Measuring.
  • Enter NextWork Projects as the account name.
  • Review the data-sharing settings.
  • Keep or change each data-sharing setting according to your preference.
  • Click Next.

What is the account for?

An Analytics account is the top-level container for properties. Your property holds the reports and settings for this specific experiment.

  • Enter GA4 Click Tracker as the property name.
  • Select your reporting time zone.
  • Select your reporting currency.
  • Click Next.
  • Select the industry category that best fits your work.
  • Select your business size.
  • Click Next.

The last screen tailors the starting report collection. Your event tracking works regardless of the objective you choose.

  • Select how you intend to use Google Analytics.
  • Click Create.
  • Accept the Analytics Terms of Service.
  • Accept the Data Processing Amendment if Google displays it.
  • Click Admin in the left navigation.
  • Confirm that GA4 Click Tracker appears as the selected property.
  • Select Reports in the left navigation.
  • Click Realtime.

Your first GA4 property is ready. You should see its Realtime report even though it has no activity yet.

Before the final check, do you expect both the local app and Analytics property to remain available when you switch browser tabs?

  • Return to the browser tab showing http://localhost:3000.
  • Confirm that the Next.js starter page still loads.
  • Switch back to Google Analytics.
  • Click Admin in the left navigation.
  • Confirm that your chosen property remains selected.
  • Select Reports in the left navigation.
  • Click Realtime.

You can now load a local Next.js App Router application. You can also reach the Admin and Realtime views for your GA4 property.

Can't see Admin or Realtime?

Check the property selector in the top-left corner because Google Analytics may have reopened a different property.

If Admin controls are unavailable, ask the property owner for the access level needed for later configuration.

Help me find my GA4 property views.

✔️ Awesome, I've got everything!

Your generated app is running locally. Your Analytics property is accessible through Admin and Realtime.

ⓧ I'd like to double check the full code

This setup step has no custom application code to compare. Double check the two required project artifacts instead.

  • A runnable Next.js App Router application is available on your computer.
  • An accessible GA4 property lets you view Admin and Realtime reports.

Your local app and analytics workspace are ready. Next up, you will create the server-side secret that lets your app send events directly to Google Analytics.

Create Your Measurement Protocol Secret

A browser event can disappear when an ad blocker intercepts it. Your later server request needs a trusted path through Google Analytics 4's Measurement Protocol.

The measurement ID points to your web data stream. The API secret authenticates your server request.

In this step, get ready to:
  • Record your web stream's measurement ID.
  • Create a Measurement Protocol API secret.
  • Store both values in .env.local outside version control.
Open your web data stream

A web data stream identifies the website whose activity reaches your property. Its measurement ID connects later browser events to that stream.

  • Go to Google Analytics.
  • Sign in with the Google Account that can access your GA4 property.
  • Select the correct GA4 property using the property selector.
  • Click Admin in the lower-left corner.
  • Under Data collection and modification, click Data streams.

The Data streams list shows whether this property already has a Web stream.

✔️ I see a Web stream

  • Select the Web stream for your website.
  • Find Measurement ID in Web stream details.
  • Record the measurement ID here: your GA4 web stream measurement ID.

You should see a measurement ID beginning with G-.

ⓧ I need a Web stream

  • Click Add stream.
  • Select Web.

You should see the form for creating a Web stream.

  • Enter your website's primary URL in the Website URL field.
  • Enter Tracked button in the Stream name field.

The form now shows the website address beside a recognisable stream name.

  • Keep enhanced measurement enabled.
  • Click Create stream.

You should land on Web stream details for the new stream.

  • Find Measurement ID.
  • Record the measurement ID here: your GA4 web stream measurement ID.

You should see a measurement ID beginning with G-.

Good, your web stream now has the identifier that both tracking paths will share.

Can't create a web stream?

Google Analytics requires Editor access at the property level to add a data stream. Ask the property owner to update your access.

Help me check why I cannot create a Google Analytics web data stream. You can also post in the Google Analytics Help Community.

Create and record the API secret

The secret setting sits inside Web stream details. It creates the credential that your server uses when sending events.

Keep this credential private

This next value is a live credential that deserves the same care as a password. Exposing it allows other people to send arbitrary data into your reports.

Keep the value in a trusted server environment. Never place it in browser code.

  • Click Measurement Protocol API secrets in Web stream details.
  • Click Create.

You should see a form asking for a nickname.

  • Enter nextwork-server-events as the nickname.
  • Click Create.

You should now see the new Secret value.

  • Copy the Secret value.
  • Create a secure note in your password manager or encrypted notes app.

You should see a blank secure note ready to hold both values.

  • Add a GA_MEASUREMENT_ID label followed by your GA4 web stream measurement ID.
  • Add a GA_API_SECRET label followed by the secret value you copied.

You've handled the sensitive part carefully. The credential now has a private home away from browser code.

What did you create?

The api_secret authenticates a Measurement Protocol request. Anyone who gets it can send arbitrary data to the property.

Server-side storage protects the quality of your reports.

No Secret value?

Check that you completed the nickname form. Confirm that your account has permission to edit the property.

Help me work out why Google Analytics did not create my Measurement Protocol API secret. You can also post in the Google Analytics Help Community.

Store the values locally

Your Next.js project reads local environment variables from .env.local. An ignore rule keeps that credential file out of version control.

  • Switch back to your local Next.js project in your code editor.
  • Find .gitignore in the file sidebar.
  • If .gitignore is missing, click the editor's New File control.
  • Name the new file .gitignore.

You should see .gitignore in the file sidebar.

  • Protect the local credential file by adding this line to .gitignore:
.env.local

Why ignore this file?

Git skips paths listed in .gitignore. This rule prevents .env.local from entering a commit.

  • Save .gitignore.
  • Search inside .gitignore for .env.local.

You should see one matching ignore rule.

Can't find the ignore rule?

Check that the filename starts with a dot. Confirm that .env.local appears on its own line.

Help me check why my .env.local ignore rule is missing from .gitignore.

  • Click the editor's New File control in the file sidebar.
  • Name the file .env.local.

You should see .env.local in the file sidebar.

  • Add both environment variables to .env.local by following this two-line reference. Enter your real values while you type:
GA_MEASUREMENT_ID=[[GA_MEASUREMENT_ID="your GA4 web stream measurement ID"]]
GA_API_SECRET=your-api-secret-here

What do these values do?

  • GA_MEASUREMENT_ID identifies the web stream that receives your events.
  • GA_API_SECRET gives your server permission to send Measurement Protocol events.
  • .env.local keeps both values local to this project.
  • Save .env.local.
  • Close .env.local so the secret is no longer visible on your screen.

Environment values look incomplete?

Confirm that each variable sits on its own line. Check that both values appear directly after the equals signs.

Help me check the structure of my .env.local file without asking me to share the API secret.

✔️ Awesome, I've got everything!

Great, your local environment file now has both values under the correct variable names.

ⓧ I'd like to double check the full code

Your own file contains the real private value from your secure note. The reference below shows the required two-line structure.

GA_MEASUREMENT_ID=[[GA_MEASUREMENT_ID="your GA4 web stream measurement ID"]]
GA_API_SECRET=your-api-secret-here

Before you check, consider whether .env.local should appear in the source control changes.

  • Click the Source Control icon in your editor's sidebar.

You should not see .env.local under the changed files. This confirms the credential file stays out of version control.

Seeing .env.local in Source Control?

Check that .gitignore contains the exact filename .env.local. Save the ignore file before checking Source Control again.

Help me remove .env.local from my source control changes without exposing its contents.

Your measurement ID now points to the right stream. Next, you'll load the analytics tag and watch your own visit reach the Realtime report.

Load the Analytics Tag

The measurement ID is safely stored in .env.local. Your Measurement Protocol API secret stays protected in the same file.

Your Next.js app still has no browser connection to Google Analytics 4. The Google tag creates that connection so your own visit can appear in Realtime.

In this step, get ready to:
  • Start the local app.
  • Load the Google tag from the root layout.
  • Confirm your active visit in Realtime.
Start the local app

The development server gives your browser a local page to visit. That page load becomes the first signal sent after the tag is ready.

  • Start the development server from the terminal for your local Next.js project by running:
npm run dev

What does this command do?

The command starts the Next.js development server. It also watches your project files for saved changes.

Keep this process running while you edit the layout. Each saved change triggers a fresh compilation.

  • Leave the development server running in the terminal.
  • Open http://localhost:3000 in your browser.

You should see your local app load in the browser. The terminal should finish compiling without an error.

Does the development server fail?

Confirm the terminal is inside the folder containing package.json. Run the command again from that folder.

Check the terminal for a port conflict if another development server is already running.

Help me diagnose why my local Next.js development server will not start.

Load the Google tag on every route

The root layout wraps every route in the App Router. Loading analytics there gives each page access to the same measurement setup.

  • Open app/layout.tsx in your code editor.
  • Add this import immediately after the existing imports:
import Script from "next/script";

What does this import do?

The Script component is Next.js's built-in loader for third-party scripts. It controls when analytics code loads in the browser.

This import never exposes GA_API_SECRET. The API secret remains available only to server-side code.

  • Save app/layout.tsx.
  • Check the running terminal for a successful recompilation.

Your local page should remain available after the layout recompiles. This confirms the script loader is ready for the tag configuration.

Does the import break the layout?

Check that the import path is exactly next/script. Keep the import with the other imports at the top of the file.

Confirm that Script appears only once in the import section.

Help me fix the Script import in my Next.js root layout.

The tag needs one script to download gtag.js. A second script connects it to the measurement ID from .env.local.

  • Find the closing </body> tag near the bottom of app/layout.tsx.
  • Paste this block between </body> and </html>:
{/* Load analytics only when the server has a measurement ID. */}
{process.env.GA_MEASUREMENT_ID && (
  <>
    <Script
      src={`https://www.googletagmanager.com/gtag/js?id=${process.env.GA_MEASUREMENT_ID}`}
      strategy="afterInteractive"
    />
    <Script id="google-analytics" strategy="afterInteractive">
      {`
        window.dataLayer = window.dataLayer || [];
        function gtag(){dataLayer.push(arguments);}
        gtag('js', new Date());
        gtag('config', '${process.env.GA_MEASUREMENT_ID}');
      `}
    </Script>
  </>
)}

What does this tag code do?

  • The condition loads analytics only when GA_MEASUREMENT_ID has a value.
  • The first Script component downloads gtag.js after the page becomes interactive.
  • The second Script component creates the browser data layer.
  • The config command connects page activity to your GA4 web stream.
  • Save app/layout.tsx.
  • Refresh http://localhost:3000 in your browser.

Your local app should render normally after the refresh. The page load now gives the Google tag an opportunity to send an automatically collected visit.

Does the layout stop compiling?

Confirm the analytics block sits inside the returned <html> element. Place it after </body>.

Check every backtick around the inline script. A missing backtick breaks the TypeScript syntax.

Help me fix the Google tag block in my Next.js root layout.

Confirm your visit in Realtime

Realtime shows whether the browser tag is reaching the correct property. Your recent page load should register as current activity.

Before you refresh the app again, do you expect Realtime to remain empty or register your browser as active?

  • Refresh your local app in the browser.
  • Return to the Google Analytics tab from earlier.
  • Select Reports from the left menu.
  • Select Realtime from the reports menu.

Realtime may need a few minutes to process the page load. Keep the local app tab open while you wait.

You should see recent activity under the active-users cards. The Event count by Event name card should include page_view from your visit.

Still waiting for your visit?

Check that GA_MEASUREMENT_ID exactly matches the measurement ID from your GA4 web data stream. Restart the development server after correcting the value.

Temporarily disable browser extensions that block analytics requests. Refresh the local app after the extension is disabled.

Help me find why my local Next.js page_view is missing from GA4 Realtime.

That is the browser connection live. Your local visit is now reaching the GA4 property.

Your browser pageview now lands in Realtime. Next up, you will connect a named click event to the tracked button.

Track the Browser Click

Your local app is already sending page views to Google Analytics 4. That proves the Google tag in your layout can reach the property.

A page view records a visit. This step adds a custom event for the button press. The named event lets you prove that a specific interaction reached your analytics property.

In this step, get ready to:
  • Turn the page into an interactive client component.
  • Connect a tracked button to the browser analytics helper.
  • Verify the named browser event in Realtime.
Build the tracked button

Interactive code needs to run in the browser. In Next.js, the 'use client' directive creates a Client Component for that work.

The browser helper calls gtag() when someone presses the button. The call sends the project event named browser_click.

  • Select app/page.tsx in your editor's file sidebar.
  • Replace the file's contents with this client-side page:
'use client'

declare global {
  interface Window {
    // The root layout creates this function when the Google tag loads.
    gtag?: (command: 'event', eventName: string) => void
  }
}

// Send the named interaction through the browser tag.
function trackBrowserClick() {
  window.gtag?.('event', 'browser_click')
}

// Render the control that starts browser-side tracking.
export default function Home() {
  return (
    <main>
      <h1>Browser click tracking</h1>
      <p>Send a click from your browser to Google Analytics.</p>
      <button type="button" onClick={trackBrowserClick}>
        Send tracked click
      </button>
    </main>
  )
}

What does this code do?

  • The 'use client' directive makes app/page.tsx a Client Component. Browser click handlers can run inside it.
  • The Window declaration describes the gtag() function that the root layout loads.
  • The trackBrowserClick() helper sends browser_click through the browser tag.
  • The onClick handler connects the button to the helper. Each press starts one browser event.
  • Save app/page.tsx.
  • Refresh the local app in your browser.

You should see the heading Browser click tracking above a button labelled Send tracked click.

Page not showing the tracked button?

  • Confirm 'use client' is the first line in app/page.tsx.
  • Check the terminal running your local app for a TypeScript or JSX error.
  • Compare app/page.tsx with the complete file below.

Help me troubleshoot why my tracked button is not rendering.

✔️ Awesome, I've got everything!

Your tracked page now matches the finished browser code.

ⓧ I'd like to double check the full code

'use client'

declare global {
  interface Window {
    // The root layout creates this function when the Google tag loads.
    gtag?: (command: 'event', eventName: string) => void
  }
}

// Send the named interaction through the browser tag.
function trackBrowserClick() {
  window.gtag?.('event', 'browser_click')
}

// Render the control that starts browser-side tracking.
export default function Home() {
  return (
    <main>
      <h1>Browser click tracking</h1>
      <p>Send a click from your browser to Google Analytics.</p>
      <button type="button" onClick={trackBrowserClick}>
        Send tracked click
      </button>
    </main>
  )
}
Prove the browser event arrives

The Realtime report groups recently received events in the Event count by Event name card. This gives you one place to test the browser path.

Before you click, which event name should this button send?

A short delay is normal while Realtime processes the click.

  • Click Send tracked click in your local app.
  • Switch back to the Google Analytics Realtime report from earlier.
  • Locate the Event count by Event name card.

You should see browser_click listed with an event count. That entry proves the button called your helper and the browser tag delivered the event.

Still missing browser_click?

  • Wait briefly for the Realtime report to update.
  • Check that browser_click uses the same spelling in app/page.tsx.
  • Confirm your root layout still loads the Google tag from the measurement ID in .env.local.
  • Refresh the local app.
  • Click Send tracked click again.

Help me diagnose why browser_click is missing from Realtime.

Your browser tracking path now records a named button interaction. Next, you'll add the server path that sends an event without relying on the browser tag.

Send the Server-Side Event

Your tracked button already sends browser_click through the Google tag. That proved your Google Analytics 4 property can hear the browser.

Now the server needs its own path. A Next.js API route keeps the secret in .env.local. The Measurement Protocol carries server_click directly to Google.

In this step, get ready to:
  • Create a server-side route that forwards Measurement Protocol payloads.
  • Post a server_click payload to the local route with curl.
  • Confirm the server event appears in the Realtime report.

Why use a server route?

Your browser only calls /api/track. The server adds GA_API_SECRET when it contacts Google.

This boundary keeps the credential inside server-side code. The secret never appears in the browser request.

Create the server route

A route handler can receive the same JSON shape that Google Analytics expects. It forwards that payload with the server-only measurement ID and API secret.

  • In the code editor you used for the local project, create app/api/track/route.ts with your editor's new-file control.
  • Paste this route into app/api/track/route.ts:
export async function POST(request: Request) {
  // Read the server-only credentials from .env.local.
  const measurementId = process.env.GA_MEASUREMENT_ID;
  const apiSecret = process.env.GA_API_SECRET;
  const payload = await request.json();

  if (!measurementId || !apiSecret) {
    return Response.json(
      { error: "Missing Google Analytics configuration." },
      { status: 500 },
    );
  }

  // Forward the payload without exposing the API secret to the browser.
  const response = await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    },
  );

  return Response.json({ ok: response.ok });
}

What does this route do?

  • The environment lookups read GA_MEASUREMENT_ID plus GA_API_SECRET inside the server process.
  • The request.json() call reads the Measurement Protocol payload sent to the route.
  • The fetch() call forwards that payload to the Google Analytics collection endpoint.
  • The local response reports whether the collection endpoint returned a successful status.
  • Save app/api/track/route.ts.
  • Switch back to the terminal running your app.

Your development server should stay running without a compilation error. This confirms Next.js can load the new route.

Route not compiling?

Check that the file lives at app/api/track/route.ts. Compare every bracket with the reference below.

Make sure the file uses the .ts extension. A different location does not create the expected /api/track route.

help me fix my Next.js route file.

✔️ Awesome, I've got everything!

Your saved route matches the complete server handler.

ⓧ I'd like to double check the full code

export async function POST(request: Request) {
  // Read the server-only credentials from .env.local.
  const measurementId = process.env.GA_MEASUREMENT_ID;
  const apiSecret = process.env.GA_API_SECRET;
  const payload = await request.json();

  if (!measurementId || !apiSecret) {
    return Response.json(
      { error: "Missing Google Analytics configuration." },
      { status: 500 },
    );
  }

  // Forward the payload without exposing the API secret to the browser.
  const response = await fetch(
    `https://www.google-analytics.com/mp/collect?measurement_id=${measurementId}&api_secret=${apiSecret}`,
    {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    },
  );

  return Response.json({ ok: response.ok });
}
Post the test event

A web Measurement Protocol payload identifies a user instance with client_id. Its events array carries the named actions.

Before you run this request, what should a successful local route response contain?

  • Send server_click to the local route by running this command:
curl --request POST http://localhost:3000/api/track --header "Content-Type: application/json" --data '{"client_id":"12345.67890","events":[{"name":"server_click"}]}'

You should see {"ok":true} in the terminal. That response confirms your server reached the Google Analytics collection endpoint.

Good work. Your local route can now forward a Measurement Protocol payload without revealing the API secret.

Request not reaching the route?

Confirm the terminal from earlier still shows your app running. A stopped development server cannot receive the request.

If the route reports a configuration problem, check that GA_MEASUREMENT_ID plus GA_API_SECRET still exist in .env.local.

help me troubleshoot my local analytics route request.

Confirm the Realtime event

Realtime can take a short moment to catch up. A brief pause here is normal.

Before you check Realtime, do you expect server_click to appear separately or change the browser_click count?

  • Switch back to the Google Analytics Realtime report from earlier.
  • Check Event count by Event name for server_click.

That's the second path working. Your Realtime report now shows browser_click from the browser beside server_click from the server.

Server event still missing?

Confirm the terminal returned {"ok":true}. Check that Realtime belongs to the same web stream referenced by GA_MEASUREMENT_ID.

Look for the exact lowercase name server_click. A different spelling creates a separate event name.

help me find my missing server_click event.

Your browser path and server path now reach the same Realtime report. In the next step, you'll send this payload to Google's debug endpoint to read its validation messages.

Validate the Measurement Protocol Payload

Your Next.js route now sends server_click from the server. Google Analytics Realtime confirms that the event reaches your property.

A successful collection request does not expose payload mistakes. The Measurement Protocol validation server returns structural feedback for the same payload.

In this step, you will send your existing payload to Google's debug endpoint. You will read the returned validation messages.

In this step, get ready to:
  • Send the existing server_click payload to the validation server.
  • Read the returned validationMessages array.
  • Confirm that the payload has no structural issues.
Understand the validation response

The debug endpoint changes only the request destination. Its JSON response reports structural issues in the payload.

What Does the Response Contain?

  • The validationMessages array holds any issues found during validation.
  • The fieldPath value identifies the part of the payload with an issue.
  • The description value explains what needs attention.
  • The validationCode value classifies the issue.
Send the payload to the debug endpoint

Your API secret stays in .env.local because the command reads it without printing its value.

  • Return to the terminal from your earlier curl test.
  • Copy the client_id value from your successful payload into your client_id from the previous curl test.
  • Select the tab that matches your terminal.

macOS

  • Send the existing payload to the validation server by running this command:

Before you run this, do you expect the valid payload to return a message or an empty list?

source .env.local

curl -X POST \
  "https://www.google-analytics.com/debug/mp/collect?measurement_id=${GA_MEASUREMENT_ID}&api_secret=${GA_API_SECRET}" \
  -H "Content-Type: application/json" \
  -d '{
    "client_id": "[[CLIENT_ID="your client_id from the previous curl test"]]",
    "events": [
      {
        "name": "server_click"
      }
    ]
  }'

What Does This Command Do?

  • The source command reads the measurement ID and API secret from .env.local.
  • The curl command sends the existing payload to /debug/mp/collect.
  • The request keeps the same client_id value. It also keeps the server_click event name.

Windows

  • Send the existing payload to the validation server by running these PowerShell commands:

Before you run this, do you expect the valid payload to return a message or an empty list?

$measurementId = (Get-Content .env.local | Where-Object { $_ -like 'GA_MEASUREMENT_ID=*' }).Split('=', 2)[1]
$apiSecret = (Get-Content .env.local | Where-Object { $_ -like 'GA_API_SECRET=*' }).Split('=', 2)[1]

$body = @{
  client_id = '[[CLIENT_ID="your client_id from the previous curl test"]]'
  events = @(
    @{
      name = 'server_click'
    }
  )
} | ConvertTo-Json -Depth 4

$response = Invoke-RestMethod -Method Post `
  -Uri "https://www.google-analytics.com/debug/mp/collect?measurement_id=$measurementId&api_secret=$apiSecret" `
  -ContentType "application/json" `
  -Body $body

$response | ConvertTo-Json -Depth 4

What Do These Commands Do?

  • The Get-Content commands read the measurement ID and API secret from .env.local.
  • The ConvertTo-Json command builds the same payload used in your earlier test.
  • The Invoke-RestMethod command posts that payload to /debug/mp/collect.
  • The final command displays the validation response as JSON.

You should see validationMessages with [] as its value. That is the check complete. The debug server found no structural issues in your server_click payload.

Seeing Validation Messages?

  • Read fieldPath to locate the field that needs attention.
  • Use description to identify the required correction.
  • Compare the client_id value with the successful payload from your previous test.
  • Confirm that the event name is exactly server_click.

help me understand my Measurement Protocol validationMessages response

The validation server checks the payload structure only. It does not add this test event to your Realtime report.

Your server payload now has a clean validation result. Next, you will compare browser tracking with server tracking while an ad blocker is active.

Compare Events With an Ad Blocker

Your Next.js browser path has reached Google Analytics 4. Your server path has reached it too.

The final question is whether the same click stays measurable when an ad blocker filters browser traffic. This step gives you a side-by-side test.

In this step, get ready to:
  • Connect the tracked button to both analytics paths.
  • Enable an ad blocker for your local app.
  • Compare the two event counts in Realtime.
Send one click through both paths

A fair comparison needs one interaction to trigger both delivery paths. Your browser helper already sends browser_click.

The existing /api/track route sends server_click through the Measurement Protocol.

  • In your local Next.js project, switch back to the component containing the tracked button.
  • Find the click handler that calls the browser analytics helper.
  • Extend the handler with a POST request to /api/track using the same client_id value from your successful curl test.
  • Reuse the payload shape accepted by the existing server route.
  • Save the tracked button component.

Why use one button?

Both sends now start from the same interaction. Any difference you observe comes from the delivery path used by that click.

The browser helper depends on a request leaving the browser. The server route forwards its event from your application server.

  • Return to the local app in your browser.
  • Refresh the page.
  • Open your browser's developer tools from the browser menu.
  • Select the panel that lists network requests.
  • Clear the existing request list.

Before you click, do you expect one interaction to create one request or two requests?

  • Click the tracked button once.

You should see a browser analytics request in the request list. You should also see a successful request to /api/track.

  • Return to the Realtime report from earlier.
  • Give Realtime a few seconds to process the new events.
  • Check Event count by Event name for updated activity.

You should see activity for both browser_click and server_click. That gives you a clean baseline from one tracked click.

  • Record the current browser_click count here: your current browser_click count.
  • Record the current server_click count here: your current server_click count.

Only seeing one event?

  • Confirm the click handler still calls the browser analytics helper.
  • Confirm the same handler posts the earlier payload shape to /api/track.
  • Disable any active blocking for this baseline test.

Ask for help with diagnosing why one tracking path is missing.

Block the browser path and compare

An ad blocker applies filtering rules to requests made by the browser. This creates a controlled test for the two delivery paths.

Finding the site control can be fiddly because browser extensions arrange their controls differently.

  • Open your ad blocker's control from the browser toolbar.
  • Enable blocking for the local app's current address.
  • Confirm the blocker reports active protection for the current address.
  • Return to the local app from earlier.
  • Clear the existing browser request list.

Before you click, which delivery path do you think still reaches Realtime under browser filtering?

  • Click the tracked button once.

You should see the browser analytics request blocked or absent. You should still see a successful request to /api/track.

  • Return to the Realtime report.
  • Give Realtime a few seconds to process the server event.
  • Compare the current browser_click count with your current browser_click count.
  • Compare the current server_click count with your current server_click count.

You should see server_click rise above your current server_click count. The browser_click count should stay at your current browser_click count.

You have now seen the measurement gap directly. The server path preserved the click after browser analytics was filtered.

Did the comparison look different?

  • Confirm the ad blocker is active for the local app's current address.
  • Check the request list for a blocked or missing browser analytics request.
  • Inspect the /api/track request for a successful response.

Ask for help with working out why the blocked comparison produced different counts.

Secret mission

Run a Controlled Delivery Test

Turn your ad blocker comparison into a controlled experiment. Capture event count changes with blocking active. Repeat the test with blocking paused to prove which delivery path survives.

Clean Up Your Resources

Clean Up Your Resources

Your local Next.js app and Google Analytics 4 tests create no ongoing charges. Choose whether to keep the project available, pause the running app, or remove the resources you created.

Resources you used:

  • The local Next.js project folder from earlier.
  • The local .env.local file inside the project folder.
  • The Measurement Protocol API secret created for the GA4 web data stream.
  • The local screenshot or secure note containing the measurement ID and API secret.

Rotate the Secret Before Publishing

The GA_API_SECRET value authenticates server-side event requests. A published copy of the test value would expose that access.

  • Delete the test API secret from the same GA4 web data stream before publishing the project.
  • Remove the GA_API_SECRET line from .env.local before sharing the folder.
  • Create a fresh server-only API secret when you resume event testing.

Keep everything running

No action needed. Choose this if you plan to keep testing the browser and server events.

  • Keep the local Next.js project folder on your computer.
  • Keep .env.local out of version control.
  • Store the screenshot or secure note somewhere only you can access.
  • Use your browser extension controls to disable the ad blocker whenever you want ordinary browser analytics.

Pause - I'll come back to this later

Shut down the local process to free up memory. Your project files and GA4 setup stay ready for another comparison.

  • Stop the development server by pressing Ctrl+C in its terminal.
  • Use your browser extension controls to disable the ad blocker for the local app.
  • Keep the local Next.js project folder in its current location.
  • Keep the API secret in .env.local only while the folder remains private.

Delete - I don't want to use this again

Removing this project's local files and test secret is permanent. Your existing GA4 property and web data stream stay intact.

  • Stop the development server by pressing Ctrl+C in its terminal if it is still running.
  • Return to the API secret screen for the GA4 web data stream from earlier.
  • Delete the newly created Measurement Protocol API secret.
  • Confirm that the secret no longer appears for the web data stream.
  • Delete the local screenshot or secure note using the same app where you stored it.
  • Confirm that the full API secret no longer appears in that app.
  • Choose your operating system below to delete the local Next.js project folder.

macOS

  • Use the macOS file browser to locate the local Next.js project folder from earlier.
  • Move the project folder to the system's deleted-items area.
  • Empty the deleted-items area to remove the project folder permanently.
  • Return to the folder's previous location in the macOS file browser.

You should no longer see the local Next.js project folder in that location.

Windows

  • Use the Windows file browser to locate the local Next.js project folder from earlier.
  • Move the project folder to the system's deleted-items area.
  • Empty the deleted-items area to remove the project folder permanently.
  • Return to the folder's previous location in the Windows file browser.

You should no longer see the local Next.js project folder in that location.

Cleanup complete. Your local project files and test credential are gone.

Nice Work!

Nice Work!

Strong finish! Your Next.js app now sends one click through browser tracking plus a server-side Measurement Protocol route. Google Analytics 4 shows which path survives when an ad blocker interrupts the browser request.

You've learned how to:

  • Load the Google tag from app/layout.tsx. Confirm your own visit in the Realtime report.
  • Send browser_click from the tracked button through browser event tracking. Send server_click from app/api/track/route.ts through server-side tracking.
  • Read debug validation messages for the server payload. Use an ad blocker to compare which event path still reaches Google Analytics.
  • Secret Mission: Take on the optional challenge to extend this two-path tracking experiment.

Ready to quiz yourself?