Google Analytics 4 w Next.js App Router — konfiguracja z gtag i @next/third-parties

Jak poprawnie wdrożyć GA4 w Next.js App Router: gtag, @next/third-parties, page_view przy client-side navigation, consent mode v2 i custom events bez chaosu w danych.

Opublikowano

18 października 2025 09:09

Czytanie

7 min czytania

Aktualizacja

15 kwietnia 2026 11:52

Integracja Google Analytics 4 w Next.js App Router to coś więcej niż wklejenie snippetu w <head>, ponieważ nawigacja client-side, React Server Components, Consent Mode v2 i wybór między ręcznym gtag.js, a @next/third-parties sprawiają, że łatwo wprowadzić analitykę pozornie poprawną, ale w praktyce generującą zdublowane page views albo niepełne dane - nawiasem mówiąc, obie te sytuacje zdarzyły mi się w przeszłości. Ten artykuł łączy szeroki setup GA4 z realnymi niuansami App Router, a jeśli nie znasz jeszcze podstaw GA4, czyli Google Analytics 4, to aktualna wersja platformy analitycznej Google do pomiaru zdarzeń i zachowań użytkowników., zacznij od Google Analytics 4 — podstawy, eventy, konwersje.

Krótka odpowiedź: GA4 w Next.js można zintegrować na dwa sposoby; pierwszy z nich to ręczne z gtag.js (pełna kontrola, wymagane dla Consent Mode i SPA, czyli Single Page Application, działa bez pełnego przeładowania dokumentu przy każdej nawigacji. page views) lub przez pakiet @next/third-parties/google - jest to sposób szybki, ale zdecydowanie mniej elastyczny. W obu przypadkach kluczowe jest wyłączenie automatycznych pageviews (send_page_view: false) i ręczne wysyłanie ich w useEffect reagującym na zmiany pathname. Dla stron w UE Consent Mode z domyślnymi wartościami denied musi być ustawiony przed załadowaniem skryptu gtag, a własne eventy najlepiej trzymać w jednym typowanym module. Gdy dochodzi więcej tagów i samodzielna praca marketingu bez deployów, zwykle lepszą warstwą zarządzania będzie GTM.

Najważniejsze na początek jest to, że @next/third-parties to wygodne i stabilne rozwiązanie, natomiast przy bardziej restrykcyjnym consent mode, warunkowym ładowaniu skryptu albo niestandardowej logice związanej z wysyłaniem pageview, ręczny setup nadal daje nam znacznie większą kontrolę.

Zanim zaczniesz

Potrzebujesz Measurement ID z panelu GA4. Znajdziesz go w Admin → Data Streams → wybierz stream → Measurement ID. Format to G-XXXXXXXXXX.

Trzymaj to ID w zmiennej środowiskowej:

Code
# .env.local
NEXT_PUBLIC_GA_MEASUREMENT_ID=G-XXXXXXXXXX

Prefiks NEXT_PUBLIC_ jest kluczowy — bez niego zmienna nie będzie dostępna w kodzie klienckim.

Podejście 1: Manualne z gtag.js

To klasyczne podejście, które daje pełną kontrolę nad tym, kiedy i jak ładowany jest skrypt GA4.

App Router (Next.js 13+)

Utwórz komponent GoogleAnalytics i osadź go w layout.tsx:

Code
// src/components/GoogleAnalytics.tsx
'use client'
 
import Script from 'next/script'
import { usePathname, useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
 
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
 
export default function GoogleAnalytics() {
  const pathname = usePathname()
  const searchParams = useSearchParams()
 
  useEffect(() => {
    if (!GA_MEASUREMENT_ID || typeof window.gtag !== 'function') return
 
    const url =
      pathname + (searchParams?.toString() ? `?${searchParams.toString()}` : '')
 
    window.gtag('event', 'page_view', {
      page_path: url,
      page_location: window.location.href,
      page_title: document.title,
    })
  }, [pathname, searchParams])
 
  if (!GA_MEASUREMENT_ID) return null
 
  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
        strategy="afterInteractive"
      />
      <Script
        id="google-analytics"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', '${GA_MEASUREMENT_ID}', {
              page_path: window.location.pathname,
              send_page_view: false
            });
          `,
        }}
      />
    </>
  )
}
Code
// src/app/layout.tsx
import { Suspense } from 'react'
import GoogleAnalytics from '@/components/GoogleAnalytics'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <body>
        <Suspense fallback={null}>
          <GoogleAnalytics />
        </Suspense>
        {children}
      </body>
    </html>
  )
}

Kilka istotnych detali w tym kodzie. send_page_view: false w konfiguracji gtag wyłącza automatyczne pageviews, zamiast tego wysyłamy je ręcznie w useEffect, reagując na zmiany pathname i searchParams. Bez tego przy client-side navigation (kliknięcie w Link) GA4 nie zarejestruje przejścia między stronami. strategy="afterInteractive" ładuje skrypt po hydratacji strony, co minimalizuje wpływ na Core Web Vitals to zestaw metryk Google oceniających szybkość, responsywność i stabilność wizualną strony.. Suspense wokół komponentu jest potrzebny, bo useSearchParams wymaga granicy Suspense w App Router.

Typowanie TypeScript

Dodaj typ dla gtag na obiekcie window:

Code
// src/types/gtag.d.ts
interface Window {
  gtag: (...args: [string, ...unknown[]]) => void
  dataLayer: Array<unknown>
}

Helper do wysyłania eventów

Stwórz prosty helper, żeby nie powtarzać logiki w każdym komponencie:

Code
// src/lib/analytics.ts
export const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
 
export function trackEvent(
  eventName: string,
  parameters?: Record<string, string | number | boolean>,
) {
  if (!GA_MEASUREMENT_ID) return
  window.gtag('event', eventName, parameters)
}
 
// Użycie w komponencie:
// trackEvent('sign_up', { method: 'email' });
// trackEvent('add_to_cart', { item_id: 'SKU_001', value: 49.99, currency: 'PLN' });

Podejście 2: @next/third-parties

Pakiet @next/third-parties to oficjalne rozwiązanie od Vercel, które upraszcza integrację z popularnymi skryptami third-party. Warto jednak śledzić aktualną dokumentację przy większych wdrożeniach.

Instalacja

Code
npm install @next/third-parties

Podstawowa konfiguracja

Code
// src/app/layout.tsx
import { GoogleAnalytics } from '@next/third-parties/google'
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="pl">
      <body>
        {children}
        <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID!} />
      </body>
    </html>
  )
}

To wszystko — komponent automatycznie ładuje skrypt gtag i konfiguruje go z Twoim Measurement ID.

Wysyłanie eventów

Pakiet eksportuje helper sendGAEvent:

Code
'use client'
 
import { sendGAEvent } from '@next/third-parties/google'
 
export function SignUpButton() {
  return (
    <button
      onClick={() => {
        sendGAEvent('event', 'sign_up', { method: 'email' })
      }}
    >
      Zarejestruj się
    </button>
  )
}

Ograniczenia @next/third-parties

@next/third-parties jest wygodny, ale ma swoje ograniczenia. Nie daje takiej kontroli nad momentem inicjalizacji Google tag, jak ręczny setup, co jest problematyczne, jeśli potrzebujesz bardziej rygorystycznego consent mode. Do tego łatwo dojść do momentu, w którym i tak trzeba dopisać własną logikę do warunkowego ładowania, page_view albo dodatkowych integracji.

Dla stron w UE Consent Mode to konieczność. Oto jak go zaimplementować z manualnym podejściem:

Code
// src/components/GoogleAnalytics.tsx
'use client'
 
import Script from 'next/script'
 
const GA_MEASUREMENT_ID = process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID
 
export default function GoogleAnalytics() {
  if (!GA_MEASUREMENT_ID) return null
 
  return (
    <>
      {/* Consent defaults MUST load before gtag */}
      <Script
        id="consent-defaults"
        strategy="beforeInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('consent', 'default', {
              analytics_storage: 'denied',
              ad_storage: 'denied',
              ad_user_data: 'denied',
              ad_personalization: 'denied',
              functionality_storage: 'granted',
              security_storage: 'granted',
              wait_for_update: 500
            });
          `,
        }}
      />
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${GA_MEASUREMENT_ID}`}
        strategy="afterInteractive"
      />
      <Script
        id="google-analytics-config"
        strategy="afterInteractive"
        dangerouslySetInnerHTML={{
          __html: `
            gtag('js', new Date());
            gtag('config', '${GA_MEASUREMENT_ID}');
          `,
        }}
      />
    </>
  )
}

Następnie, kiedy użytkownik zaakceptuje cookies w Twoim banerze:

Code
// src/lib/consent.ts
export function grantAnalyticsConsent() {
  window.gtag('consent', 'update', {
    analytics_storage: 'granted',
  })
}
 
export function grantAllConsent() {
  window.gtag('consent', 'update', {
    analytics_storage: 'granted',
    ad_storage: 'granted',
    ad_user_data: 'granted',
    ad_personalization: 'granted',
  })
}

Kluczową rzeczą jest to, że najpierw musisz ustawić domyślne zgody (np. wszystko zablokowane), zanim załaduje się główny kod GA4. Dopiero potem, gdy użytkownik wyrazi zgodę, aktualizujesz te ustawienia. Robiąc to w złej kolejności, GA4 może zacząć zbierać dane, zanim użytkownik się zgodzi, co jest niezgodne z obowiązującym prawem.

Warunki ładowania skryptu

Jeśli wolisz, żeby kod GA4 w ogóle się nie ładował, dopóki użytkownik nie wyrazi zgody, możesz to zrobić tak:

Code
'use client'
 
import Script from 'next/script'
import { useEffect, useState } from 'react'
 
export default function GoogleAnalytics() {
  const [hasConsent, setHasConsent] = useState(false)
 
  useEffect(() => {
    const consent = localStorage.getItem('analytics-consent')
    if (consent === 'granted') {
      setHasConsent(true)
    }
 
    // Nasłuchuj na zmiany zgody z cookie bannera
    const handler = () => setHasConsent(true)
    window.addEventListener('analytics-consent-granted', handler)
    return () =>
      window.removeEventListener('analytics-consent-granted', handler)
  }, [])
 
  if (!hasConsent) return null
 
  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${process.env.NEXT_PUBLIC_GA_MEASUREMENT_ID}`}
        strategy="afterInteractive"
      />
      {/* ... config script */}
    </>
  )
}

Testowanie i debugowanie

DebugView

Włącz specjalny tryb debug, dodając parametr w konfiguracji:

Code
gtag('config', 'G-XXXXXXXXXX', {
  debug_mode: true,
})

Albo automatycznie w trybie deweloperskim:

Code
gtag('config', GA_MEASUREMENT_ID, {
  debug_mode: process.env.NODE_ENV === 'development',
})

Po włączeniu powyższego trybu, wszystkie zdarzenia, które wysyłasz, pojawią się w czasie rzeczywistym w sekcji "DebugView" w panelu GA4.

Sprawdzanie w DevTools

Otwórz narzędzia deweloperskie (zazwyczaj F12), przejdź do zakładki "Network" i wpisz "collect" w polu wyszukiwania - zobaczysz tam wszystkie zapytania wysyłane do serwerów Google Analytics. Właśnie w ten sposób, szybko możemy sprawdzić, czy zdarzenia w ogóle są wysyłane.

Tag Assistant

Google Tag Assistant (rozszerzenie Chrome albo wersja webowa na tagassistant.google.com) pozwala podejrzeć, jakie eventy są wysyłane i czy konfiguracja jest poprawna.

Typowe błędy

Brak pageviews przy client-side navigation to najczęstszy problem. Dzieje się tak ponieważ Next.js przy nawigacji przez <Link> nie ładuje strony od nowa, więc gtag nie wysyła automatycznie pageview. Rozwiązaniem jest ręczne wysyłanie page_view w useEffect reagującym na zmiany pathname.

Podwójne pageviews — jeśli masz ustawione send_page_view: true (domyślne) i jednocześnie wysyłasz ręcznie pageviews w useEffect, dostajesz duplikaty. By to naprawić, ustaw w konfiguracji send_page_view: false.

Brak danych w development — wtyczki blokujące reklamy i rozszerzenia prywatności blokują requesty do google-analytics.com. W trakcie developmentu testuj w oknie incognito z wyłączonymi rozszerzeniami albo korzystaj z DebugView.

Zmienne środowiskowe undefined — zapomniane NEXT_PUBLIC_ prefiks lub brak .env.local w katalogu projektu. Pamiętaj, że po dodaniu zmiennej musisz zrestartować serwer deweloperski.

Które podejście wybrać?

Wybór @next/third-parties ma większy sens, jeśli chcesz szybko dodać GA4 bez szerokiej customizacji, a Twoja strona nie wymaga złożonego warunkowego ładowania skryptu.

Podejście manualne z gtag sprawdzi się lepiej, gdy potrzebujesz pełnej kontroli nad Consent Mode (strony w UE, więc też PL), chcesz warunkowo ładować skrypt na podstawie zgody użytkownika, potrzebujesz niestandardowej logiki śledzenia pageviews lub integrujesz GA4 z innymi narzędziami (np. Google Tag Manager).

Dla większości polskich projektów — gdzie RODO wymaga Consent Mode — manualne podejście daje więcej kontroli i jest bezpieczniejszym wyborem - właśnie to rekomenduje.

FAQ

Dlaczego GA4 nie rejestruje przejść między stronami w Next.js?

Next.js używa client-side navigation — przy kliknięciu w <Link> strona nie jest przeładowywana, więc gtag nie wysyła automatycznie pageview. Rozwiązaniem jest ustawienie send_page_view: false w konfiguracji gtag i ręczne wysyłanie eventu page_view w useEffect, który reaguje na zmiany pathname i searchParams z hooków next/navigation.

Czy @next/third-parties jest gotowe do użycia produkcyjnego?

Pakiet działa stabilnie i jest oficjalnie rozwijany przez Vercel. Do prostego wdrożenia GA4 bez specjalnych wymagań może być wystarczający, ale jeśli potrzebujesz precyzyjnej kontroli nad Consent Mode, warunkowego ładowania skryptu na podstawie zgody lub niestandardowej logiki pageviews, lepiej wybrać manualne podejście z gtag.

Consent Mode wymaga ustawienia domyślnych wartości (denied) przed załadowaniem skryptu gtag. W Next.js oznacza to użycie strategy="beforeInteractive" dla skryptu z domyślnymi zgodami i strategy="afterInteractive" dla samego gtag. Po wyrażeniu zgody przez użytkownika wywołujesz gtag('consent', 'update', {...}) z wartościami granted. Kolejność ładowania jest krytyczna — odwrócenie jej powoduje zbieranie danych przed wyrażeniem zgody.

Gdzie znajdę Measurement ID dla GA4?

Measurement ID znajdziesz w panelu Google Analytics w sekcji Admin → Data Streams → kliknij na wybrany stream → Measurement ID. Format to G-XXXXXXXXXX, wklej to ID do zmiennej środowiskowej NEXT_PUBLIC_GA_MEASUREMENT_ID w pliku .env.local — prefiks NEXT_PUBLIC_ jest obowiązkowy, bez niego zmienna nie będzie dostępna w kodzie klienckim.

Dlaczego widzę zduplikowane pageviews w GA4?

Duplikaty pageviews wynikają najczęściej z jednoczesnego aktywnego ustawienia send_page_view: true (domyślne) w konfiguracji gtag i ręcznego wysyłania pageview w useEffect. Rozwiązaniem jest ustawienie send_page_view: false w konfiguracji gtag, żeby wyłączyć automatyczne pageviews i polegać wyłącznie na ręcznym wysyłaniu.

Czy GA4 działa poprawnie w React Server Components?

Tak, ale sam tag GA4 i każde odwołanie do window, document albo gtag() musi żyć po stronie klienta. Server Components mogą bez problemu renderować HTML strony, natomiast tracking osadzasz w Client Componentach lub helperach wywoływanych z klienta.

Jak testować GA4 w środowisku lokalnym?

Włącz tryb debugowania w konfiguracji GA4 (debug_mode: true), a zdarzenia pojawią się w "DebugView" w panelu GA4. Alternatywnie sprawdź zakładkę Network w DevTools i filtruj po „collect", a zobaczysz requesty do google-analytics.com/g/collect. Pamiętaj, że wtyczki blokujące reklamy mogą to utrudniać, więc testuj w trybie incognito z wyłączonymi rozszerzeniami.

Czy warto używać GA4 z GTM zamiast bezpośredniej integracji?

To zależy od potrzeb, ponieważ bezpośrednia integracja gtag jest prostsza i daje pełną kontrolę developerowi. GTM, czyli Google Tag Manager, pozwala zarządzać tagami i skryptami marketingowymi bez każdej zmiany w kodzie aplikacji. warto rozważyć, gdy masz kilka narzędzi analitycznych i marketingowych, zespół marketingowy potrzebuje samodzielnie dodawać tagi bez deployów, lub gdy chcesz centralnie zarządzać zgodami dla wielu tagów. Szczegóły tej decyzji opisuję w artykule o GTM w Next.js.

Podsumowanie

Konfiguracja GA4 w Next.js nie jest tak czarną magią, jak może się początkowo wydawać, ale wymaga zrozumienia kilku specyficznych rzeczy, client-side routing, kolejność ładowania skryptów, Consent Mode. Ręczna instalacja daje Ci najwięcej kontroli, a pakiet @next/third-parties jest szybszy, ale mniej elastyczny.

Najważniejsze to pamiętać o trzech sprawach:

  • Poprawne śledzenie odsłon strony przy przechodzeniu między podstronami.
  • Wdrożenie Consent Mode dla użytkowników z Unii Europejskiej.
  • Zawsze testuj swoje ustawienia za pomocą "DebugView", zanim wrzucisz je na działającą stronę.

Źródła i dokumentacja


Chcesz wiedzieć, jak wykorzystać zebrane dane? Sprawdź praktyczne przypadki decyzji produktowych na podstawie analityki lub poznaj Hotjar — heatmapy i nagrania sesji.

Pracuję z tym zawodowo.

Jeśli chcesz połączyć SEO, analitykę, Google Ads i warstwę techniczną strony w jeden sensowny system wzrostu, skontaktuj się ze mną. Pomagam układać wdrożenia, które nie kończą się na samym tagowaniu, ale wspierają widoczność, pomiar i konwersję.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab