Przejdź do treści

Google Ads Conversion Tracking w Next.js

Śledzenie konwersji Google Ads w Next.js bez luki w danych. Poznaj setup: gtag, enhanced conversions oraz server-side tracking bezpośrednio przez Ads API.

Maciej Sala

Founder StriveLab

11 min czytaniaOpublikowano 12 listopada 2025 (Aktualizacja 14 lipca 2026)

Po stronie dewelopera sprowadza się to do jednego: wdrożyć tracking tak, żeby dane były kompletne, dokładne i zgodne z GDPR. Brzmi banalnie, a właśnie tu najczęściej coś przecieka.

Jak działa Google Ads conversion tracking?

Zasada działania nie należy do skomplikowanych: użytkownik klika reklamę i trafia na Twoją stronę, a gdy wykona akcję, na której Ci zależy (np. zakup, formularz, rejestracja), wtedy gtag wysyła event konwersji z powrotem do Google Ads. Tam konwersja zostaje przypisana do konkretnej kampanii, reklamy i słowa kluczowego, dzięki czemu algorytm „rozumie", które kliknięcia przynoszą pieniądze, a które tylko je wydają.

Cała ta układanka opiera się na jednym parametrze, jakim jest (Google Click Identifier), doklejanym do URL po kliknięciu reklamy (?gclid=abc123). To on łączy kliknięcie z konwersją. Chodzi o to, żeby przechwycić go od razu po wejściu i zapisać tak, by przetrwał dalszą nawigację.

Warto wiedzieć, że gtag robi część tej roboty za Ciebie: z włączonym conversion linkerem zapisuje GCLID w first-party cookie _gcl_aw (w formacie GCL.<timestamp>.<gclid>). Dla czysto client-side'owego pomiaru to zwykle odpowiednie rozwiązanie. Ręcznego przechwycenia identyfikatora potrzebujesz wtedy (krok 4), gdy chcesz mieć GCLID we własnej bazie obok zamówienia i wysyłać konwersje server-side, a także, gdy adblocker zablokował sam skrypt gtag, ale parametru w URL już nie.

Diagram
Dwie równoległe ścieżki pomiaru: client-side przez gtag i server-side przez Google Ads API, spięte wspólnym transaction_id / orderId.

Dedykowany tag Google Ads czy import konwersji z GA4?

Zanim napiszesz linijkę kodu, podejmij jedną decyzję architektoniczną. Musisz zdecydować, skąd Google Ads ma brać konwersje. Są dwie drogi.

Import z jest atrakcyjny, ponieważ eventy purchase czy generate_lead często już istnieją i wystarczy połączyć konta, by zaimportować je jako konwersje. Jest jednak jeden haczyk: dane z GA4 docierają do Google Ads z opóźnieniem (zwykle liczonym w godzinach), przechodzą przez model atrybucji GA4 i nie wspierają pełnego zakresu funkcji dedykowanego tagu (wliczając enhanced conversions for web).

Dedykowany tag Google Ads, czyli ten, który wdrażamy w tym artykule. Wysyła konwersję bezpośrednio, niemalże w czasie rzeczywistym. Dla strategii automatycznego bidowania (tCPA, tROAS) świeżość sygnału ma znaczenie, ponieważ algorytm uczy się na bieżąco. Do tego dochodzą enhanced conversions i możliwość dosyłania konwersji przez API.

W praktyce rozsądny układ wygląda tak: kluczowe konwersje (zakup, lead) mierz dedykowanym tagiem Google Ads i ustaw je jako primary, a odpowiadające im eventy GA4 zostaw do analityki albo zaimportuj jako secondary — do obserwacji, nie do bidowania. Najgorszy scenariusz to obie ścieżki ustawione jako primary dla tej samej akcji: Google Ads policzy każdą konwersję dwa razy, a kampanie zaczną optymalizować się pod zawyżone liczby.

Krok 1: bazowy setup gtag dla Google Ads

Code
// components/GoogleAds.tsx
"use client";
 
import Script from "next/script";
 
interface GoogleAdsProps {
  conversionId: string; // format: AW-XXXXXXXXX
}
 
export function GoogleAds({ conversionId }: GoogleAdsProps) {
  if (process.env.NODE_ENV !== "production") return null;
 
  return (
    <>
      <Script
        src={`https://www.googletagmanager.com/gtag/js?id=${conversionId}`}
        strategy="afterInteractive"
      />
      <Script id="google-ads-init" strategy="afterInteractive">
        {`
          window.dataLayer = window.dataLayer || [];
          function gtag(){dataLayer.push(arguments);}
          gtag('js', new Date());
          gtag('config', '${conversionId}');
        `}
      </Script>
    </>
  );
}

Jeśli masz już GA4 gtag na stronie, nie ładuj gtag dwa razy. Dodaj Google Ads config do istniejącego gtag:

Code
// W istniejącym GoogleAnalytics.tsx
<Script id="gtag-init" strategy="afterInteractive">
  {`
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
    gtag('js', new Date());
    gtag('config', '${gaMeasurementId}', { send_page_view: false });
    gtag('config', '${adsConversionId}');
  `}
</Script>

Żeby TypeScript nie protestował przy każdym window.gtag, dodaj deklarację globalną:

Code
// types/gtag.d.ts
export {}
 
declare global {
  interface Window {
    dataLayer: unknown[]
    gtag: (...args: unknown[]) => void
  }
}

Krok 2: wysyłanie konwersji Google Ads

Konwersja zakupu purchase

Code
// lib/ads-tracking.ts
export function trackAdsConversion({
  conversionLabel,
  value,
  currency = 'PLN',
  transactionId,
}: {
  conversionLabel: string
  value: number
  currency?: string
  transactionId: string
}) {
  if (typeof window === 'undefined' || !window.gtag) return
 
  window.gtag('event', 'conversion', {
    send_to: `${process.env.NEXT_PUBLIC_ADS_CONVERSION_ID}/${conversionLabel}`,
    value,
    currency,
    transaction_id: transactionId,
  })
}
 
// Predefiniowane konwersje
export const adsConversions = {
  purchase: (orderId: string, value: number) =>
    trackAdsConversion({
      conversionLabel: 'PURCHASE_LABEL_HERE',
      value: value / 100, // centy → złotówki
      transactionId: orderId,
    }),
 
  lead: (formName: string) =>
    trackAdsConversion({
      conversionLabel: 'LEAD_LABEL_HERE',
      value: 0,
      transactionId: `lead_${formName}_${Date.now()}`,
    }),
 
  signUp: (userId: string) =>
    trackAdsConversion({
      conversionLabel: 'SIGNUP_LABEL_HERE',
      value: 0,
      transactionId: `signup_${userId}`,
    }),
}

Wysyłka konwersji na Thank You Page

Code
// app/order/[id]/thank-you/page.tsx
import { ThankYouClient } from "./ThankYouClient";
import { getOrder } from "@/lib/orders";
 
export default async function ThankYouPage({
  params,
}: {
  params: { id: string };
}) {
  const order = await getOrder(params.id);
 
  return <ThankYouClient order={order} />;
}
Code
// app/order/[id]/thank-you/ThankYouClient.tsx
"use client";
 
import { useEffect, useRef } from "react";
import { adsConversions } from "@/lib/ads-tracking";
import { analytics } from "@/lib/analytics";
 
export function ThankYouClient({ order }: { order: Order }) {
  const tracked = useRef(false);
 
  useEffect(() => {
    if (tracked.current) return;
    tracked.current = true;
 
    // Google Ads conversion
    adsConversions.purchase(order.id, order.totalValue);
 
    // GA4 purchase event (dla raportów ecommerce)
    analytics.purchase(order.id, order.totalValue);
  }, [order]);
 
  return (
    <div>
      <h1>Dziękujemy za zamówienie!</h1>
      <p>Numer zamówienia: {order.id}</p>
    </div>
  );
}

Ważne: useRef zapobiega podwójnemu wysłaniu konwersji przy React strict mode (double mount w dev).

Krok 3: Enhanced Conversions w Google Ads

Enhanced conversions to mechanizm, który wysyła zahashowane dane użytkownika (email, telefon, adres) razem z konwersją. Google wykorzystuje je do lepszego dopasowania konwersji do interakcji reklamowych, szczególnie tam, gdzie klasyczny pomiar oparty o cookies ma luki.

Dlaczego Enhanced Conversions są ważne?

Safari i Firefox od lat ograniczają klasyczny tracking reklamowy, a adblockery i ustawienia prywatności dodatkowo komplikują pomiar. Enhanced conversions mogą poprawić kompletność danych, ale nie są gwarancją konkretnego upliftu. Traktuj je jako rozszerzenie pomiaru, nie jako automatyczny „booster” wyników.

Implementacja Enhanced Conversions

Code
// lib/ads-tracking.ts – rozszerzony o enhanced conversions
export function trackEnhancedConversion({
  conversionLabel,
  value,
  currency = 'PLN',
  transactionId,
  userEmail,
  userPhone,
  firstName,
  lastName,
}: {
  conversionLabel: string
  value: number
  currency?: string
  transactionId: string
  userEmail?: string
  userPhone?: string
  firstName?: string
  lastName?: string
}) {
  if (typeof window === 'undefined' || !window.gtag) return
 
  // Ustaw user data PRZED wysłaniem konwersji
  if (userEmail || userPhone) {
    window.gtag('set', 'user_data', {
      email: userEmail?.toLowerCase().trim(),
      phone_number: userPhone, // format: +48123456789
      address: {
        first_name: firstName,
        last_name: lastName,
      },
    })
  }
 
  window.gtag('event', 'conversion', {
    send_to: `${process.env.NEXT_PUBLIC_ADS_CONVERSION_ID}/${conversionLabel}`,
    value,
    currency,
    transaction_id: transactionId,
  })
}
Code
// Użycie po zakupie
trackEnhancedConversion({
  conversionLabel: 'PURCHASE_LABEL',
  value: order.totalValue / 100,
  transactionId: order.id,
  userEmail: order.customer.email,
  userPhone: order.customer.phone,
  firstName: order.customer.firstName,
  lastName: order.customer.lastName,
})

Samo gtag('set', 'user_data', ...) nie wystarczy — enhanced conversions musisz też włączyć w config (albo zaakceptować warunki i metodę w panelu Google Ads):

Code
gtag('config', adsConversionId, { allow_enhanced_conversions: true })

Przy wdrożeniu przez Google tag lub Google może wykonać hashing po swojej stronie zgodnie z wybraną metodą implementacji. Przy integracjach API lub importach offline zakres odpowiedzialności bywa inny, więc zawsze sprawdź, czy dana ścieżka oczekuje już danych zhashowanych.

Dwie rzeczy decydują o jakości dopasowania. Po pierwsze format danych: email znormalizowany do małych liter i bez spacji (robi to toLowerCase().trim() w kodzie wyżej), telefon w formacie E.164 z prefiksem kraju (+48123456789) — numer zapisany jako 123 456 789 nie zostanie dopasowany. Po drugie consent: przesyłanie danych użytkownika do Google dla ruchu z EEA/UK/CH wymaga zgody ad_user_data, o której więcej w kroku 5. Bez niej enhanced conversions dla tych użytkowników po prostu nie zadziałają — i słusznie.

Konfiguracja Enhanced Conversions w Google Ads

  1. Google Ads → Goals → Conversions → Settings
  2. Włącz "Enhanced conversions"
  3. Wybierz "Global site tag" jako metodę implementacji
  4. Zaznacz "Provide user-provided data through JavaScript variables"

Krok 4: server-side conversion tracking

Client-side tracking może być ograniczany przez adblockery, ustawienia prywatności albo utratę identyfikatorów po drodze. W Google Ads „server-side" najczęściej oznacza nie GA4 Measurement Protocol, tylko offline/upload click conversions przez Google Ads API.

Code
// lib/ads-server/upload-conversion.ts
export async function uploadOfflineConversion({
  conversionActionId,
  gclid,
  orderId,
  conversionDateTime,
  conversionValue,
  currencyCode = 'PLN',
}: {
  conversionActionId: string
  gclid: string
  orderId: string // ten sam co transaction_id w gtag → deduplikacja
  conversionDateTime: string // format: "2026-02-19 14:30:00+01:00"
  conversionValue: number
  currencyCode?: string
}) {
  // Wymaga Google Ads API client library
  // Ten kod działa tylko server-side (API Route, Server Action)
 
  const response = await fetch(
    `https://googleads.googleapis.com/v19/customers/${customerId}:uploadClickConversions`, // sprawdź aktualną wersję API na developers.google.com/google-ads/api/docs/release-notes
    {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${accessToken}`,
        'developer-token': process.env.GOOGLE_ADS_DEVELOPER_TOKEN!,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        conversions: [
          {
            gclid,
            orderId,
            conversionAction: `customers/${customerId}/conversionActions/${conversionActionId}`,
            conversionDateTime,
            conversionValue,
            currencyCode,
          },
        ],
        partialFailure: true,
      }),
    },
  )
 
  return response.json()
}

Wartości accessToken i customerId nie pojawiają się znikąd i stanowią najtrudniejszą część implementacji po stronie serwera. accessToken generujesz z tokena odświeżającego OAuth2 (zazwyczaj za pomocą oficjalnej biblioteki google-ads-api/google-ads-node, która automatycznie odświeża token), natomiast customerId to identyfikator konta Google Ads, pozbawiony myślników. Obie te wartości przechowujesz w zmiennych środowiskowych po stronie serwera, nigdy w kodzie klienta.

Trzy pola w tym payloadzie mają niuanse, które łatwo przeoczyć:

  • orderId. Ustaw go na to samo, co transaction_id w konwersji z gtag. To warunek, żeby Google Ads potraktował client-side event i server-side upload jako jedną konwersję, a nie dwie.

  • conversionDateTime. To moment konwersji (złożenia zamówienia), nie moment przechwycenia GCLID. Musi być późniejszy niż samo kliknięcie reklamy i wymaga jawnej strefy czasowej (+01:00). Upload musi też zmieścić się w oknie konwersji danej akcji — kliknięć starszych niż skonfigurowane okno (maksymalnie 90 dni) nie ma sensu dosyłać.

  • partialFailure: true. Sprawia, że błędne konwersje w batchu nie wywracają całego requestu. Ale to oznacza, że response ze statusem 200 nie gwarantuje sukcesu: błędy siedzą w polu partialFailureError, które trzeba sprawdzić i zalogować.

Zapisywanie GCLID po stronie aplikacji

Żeby wysłać offline conversion, musisz zapisać GCLID z URL przy pierwszej wizycie:

Code
// hooks/useGclid.ts
'use client'
 
import { useSearchParams } from 'next/navigation'
import { useEffect } from 'react'
 
export function useGclidCapture() {
  const searchParams = useSearchParams()
 
  useEffect(() => {
    // Na iOS, gdy brak cookies, Google przekazuje gbraid (web → app)
    // albo wbraid (app → web) zamiast gclid — przechwyć którykolwiek.
    const clickId =
      searchParams.get('gclid') ??
      searchParams.get('gbraid') ??
      searchParams.get('wbraid')
 
    if (clickId) {
      // Zapisz w cookie z przykładowym okresem retencji.
      // Konkretne okno atrybucji i użyteczność identyfikatora zależą od setupu kampanii i typu konwersji.
      document.cookie = `_gclid=${clickId}; max-age=${90 * 24 * 60 * 60}; path=/; SameSite=Lax`
 
      // Lub w localStorage jako fallback
      localStorage.setItem('gclid', clickId)
      localStorage.setItem('gclid_timestamp', Date.now().toString())
    }
  }, [searchParams])
}

Pamiętaj, że zapis identyfikatora kliknięcia w cookie czy localStorage to też przechowywanie danych na urządzeniu użytkownika — w regionach objętych wymogiem zgody hook powinien respektować ten sam stan consentu co gtag (ad_storage), a nie działać bezwarunkowo.

Hook tylko zapisuje identyfikator. Brakującym ogniwem jest odczyt zapisanego GCLID przy konwersji i wysłanie go server-side — to ono domyka lukę w danych:

Code
// lib/gclid.ts — odczyt zapisanego identyfikatora
export function getStoredGclid() {
  if (typeof document === 'undefined') return null
 
  const own = document.cookie.match(/(?:^|;\s*)_gclid=([^;]+)/)
  if (own?.[1]) return own[1]
 
  // Fallback: cookie gtaga w formacie GCL.<timestamp>.<gclid>
  const gtagCookie = document.cookie.match(/(?:^|;\s*)_gcl_aw=([^;]+)/)
  const fromGtag = gtagCookie?.[1]?.split('.')[2]
 
  return fromGtag ?? localStorage.getItem('gclid')
}

Fallback z _gcl_aw domyka scenariusz, w którym Twój hook nie zdążył się wykonać (np. użytkownik wszedł głęboko w serwis linkiem bez parametrów, ale gtag zapisał identyfikator podczas wcześniejszej wizyty).

Code
// Na thank you page — obok client-side konwersji wyślij też upload server-side
import { getStoredGclid } from '@/lib/gclid'
 
async function sendServerConversion(order: Order) {
  const gclid = getStoredGclid()
  if (!gclid) return // brak identyfikatora → nie ma czego uploadować
 
  await fetch('/api/conversions', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      orderId: order.id,
      value: order.totalValue,
      gclid,
    }),
  })
}

Wywołaj sendServerConversion(order) w tym samym useEffect na thank you page, w którym odpala się adsConversions.purchase — obie ścieżki idą równolegle, a deduplikację załatwia wspólny identyfikator: transaction_id w gtag i orderId w uploadzie, oba równe order.id.

Code
// app/api/conversions/route.ts — odbiera GCLID i uploaduje server-side
import { NextRequest, NextResponse } from 'next/server'
import { formatInTimeZone } from 'date-fns-tz'
import { uploadOfflineConversion } from '@/lib/ads-server/upload-conversion'
 
function formatConversionDateTime(date: Date) {
  // Google Ads API wymaga formatu "yyyy-MM-dd HH:mm:ss+|-HH:mm",
  // np. "2026-07-14 14:30:00+02:00" — z jawnym offsetem strefy
  return formatInTimeZone(date, 'Europe/Warsaw', 'yyyy-MM-dd HH:mm:ssxxx')
}
 
export async function POST(request: NextRequest) {
  const { orderId, value, gclid } = await request.json()
 
  if (!gclid) {
    return NextResponse.json({ status: 'no_gclid' })
  }
 
  const result = await uploadOfflineConversion({
    conversionActionId: process.env.ADS_PURCHASE_ACTION_ID!,
    gclid,
    orderId,
    conversionDateTime: formatConversionDateTime(new Date()),
    conversionValue: value / 100,
  })
 
  // partialFailure: sukces HTTP ≠ sukces konwersji
  if (result.partialFailureError) {
    console.error('Ads upload partial failure:', result.partialFailureError)
  }
 
  return NextResponse.json(result)
}

Zwróć uwagę, że czasem konwersji jest moment złożenia zamówienia (tu: chwila obsługi requestu), a nie timestamp przechwycenia GCLID — ten drugi mówi tylko, kiedy użytkownik kliknął reklamę, i przydaje się co najwyżej do odrzucenia identyfikatorów starszych niż okno konwersji.

W produkcyjnym wdrożeniu ten endpoint warto uszczelnić: wołaj go z logiki serwerowej po potwierdzeniu płatności (np. z webhooka Stripe) zamiast z przeglądarki, a przynajmniej zweryfikuj, że orderId istnieje i nie był już uploadowany. Endpoint przyjmujący dowolne wartości konwersji z klienta to zaproszenie do zaśmiecenia danych kampanii.

Consent Mode v2 to dla Google Ads warunek gry w EEA/UK/CH. Trzy sygnały mają znaczenie: ad_storage (cookies reklamowe), ad_user_data (przesyłanie danych użytkownika do Google — bez niego nie zadziałają m.in. enhanced conversions) i ad_personalization (personalizacja i remarketing).

Najczęstszy błąd wdrożeniowy: obsłużony jest tylko consent update po kliknięciu w banner, a brakuje stanu domyślnego. wymaga, żeby consent default wykonał się przed pierwszym config — inaczej gtag przy załadowaniu strony zdąży zachować się tak, jakby zgoda była udzielona. W Next.js oznacza to umieszczenie defaultu w tym samym inline skrypcie, przed liniami config:

Code
// Rozszerzony skrypt inicjalizacyjny — default PRZED config
<Script id="gtag-init" strategy="afterInteractive">
  {`
    window.dataLayer = window.dataLayer || [];
    function gtag(){dataLayer.push(arguments);}
 
    gtag('consent', 'default', {
      ad_storage: 'denied',
      ad_user_data: 'denied',
      ad_personalization: 'denied',
      analytics_storage: 'denied',
      wait_for_update: 500,
      region: ['PL', 'DE', 'FR'] // lub pełna lista EEA/UK/CH
    });
 
    gtag('js', new Date());
    gtag('config', '${gaMeasurementId}');
    gtag('config', '${adsConversionId}');
  `}
</Script>

wait_for_update daje bannerowi 500 ms na dostarczenie zapisanej wcześniej zgody, zanim gtag zacznie wysyłać cokolwiek z domyślnym denied. Parametr region pozwala ograniczyć restrykcyjny default do regionów, które tego wymagają. Po decyzji użytkownika banner wysyła aktualizację:

Code
// Po akceptacji cookie banner
window.gtag('consent', 'update', {
  ad_storage: 'granted',
  ad_user_data: 'granted',
  ad_personalization: 'granted',
  analytics_storage: 'granted',
})

Gdy consent pozostaje denied, gtag przechodzi w tryb cookieless ping — wysyła zubożone sygnały bez identyfikatorów, na których Google może modelować konwersje (przy wystarczającym wolumenie danych). Dla utrzymania atrybucji bez cookies przydaje się jeszcze url_passthrough:

Code
gtag('set', 'url_passthrough', true)

Z tą flagą gtag dokleja GCLID do linków wewnętrznych podczas nawigacji, dzięki czemu informacja o kliknięciu reklamy nie ginie mimo braku zgody na cookies. To nadal pomiar gorszej jakości niż pełny consent — ale znacznie lepszy niż zero sygnału.

Debugowanie conversion tracking w Google Ads

Google Tag Assistant

Wejdź na tagassistant.google.com, podłącz swoją domenę i przejdź ścieżkę konwersji od wejścia z parametrem gclid po thank you page. Tag Assistant pokaże każde wywołanie gtag w kolejności — w tym stan consentu w momencie odpalenia tagu i przekazane user_data dla enhanced conversions. To najszybszy sposób, żeby wykryć klasyczny błąd kolejności: config wykonany przed consent default.

Diagnostyka konwersji w Google Ads

W panelu Google Ads każda akcja konwersji ma zakładkę diagnostyczną ze statusem tagu: czy jest aktywny, kiedy ostatnio otrzymał dane, jaki odsetek konwersji ma dołączone dane enhanced conversions i czy występują problemy z consentem. Świeżo wdrożony tag potrafi mieć status „No recent conversions" przez kilkadziesiąt godzin — to normalne opóźnienie raportowania, nie sygnał błędu.

Network tab i requesty konwersji

W DevTools filtruj requesty po googleadservices.com/pagead/conversion — to właśnie Twoje konwersje. W parametrach requestu zobaczysz value, currency_code i label; przy enhanced conversions dojdzie zahashowany blok danych użytkownika. Sprawdź też w zakładce Application, czy po wejściu z ?gclid=test123 powstało cookie _gcl_aw (i Twoje _gclid, jeśli używasz hooka z kroku 4).

Debugowanie uploadów server-side

Dla uploadów przez API najważniejsza zasada: loguj partialFailureError, bo HTTP 200 nie oznacza przyjętej konwersji. Typowe błędy to CLICK_NOT_FOUND (GCLID nie istnieje albo pochodzi z innego konta), TOO_RECENT_CONVERSION_ACTION (akcja utworzona przed chwilą — odczekaj kilka godzin) i EXPIRED_CLICK (kliknięcie poza oknem konwersji). Ręcznie spreparowany gclid=test123 zawsze zwróci CLICK_NOT_FOUND — do testów end-to-end potrzebujesz kliknięcia z prawdziwej kampanii.

Najczęstsze błędy conversion tracking

Podwójne konwersje – użytkownik odświeża thank you page i konwersja wysyła się ponownie. Rozwiązanie: transaction_id + deduplikacja.

Podwójne liczenie z dwóch źródeł – tag Google Ads i import z GA4 ustawione jako primary dla tej samej akcji. Każdy zakup liczy się dwa razy, a bidding optymalizuje się pod fikcyjne wyniki. Jedno źródło primary, reszta secondary.

Brak GCLID – jeśli URL jest przepisywany (redirect, UTM stripping), GCLID się gubi. Upewnij się, że GCLID przeżywa redirecty.

Tylko gclid, bez gbraid/wbraid – na ruchu z iOS klasyczny identyfikator często nie dociera i konwersje z urządzeń Apple systematycznie wypadają z pomiaru. Hook z kroku 4 łapie wszystkie trzy.

consent update bez consent default – banner aktualizuje zgodę, ale gtag załadował się bez zdefiniowanego stanu domyślnego, więc do momentu decyzji użytkownika działał tak, jakby zgoda była. Formalnie to brak zgodności z wymogami dla EEA/UK/CH, nawet jeśli dane „wyglądają dobrze".

Różne wartości konwersji w GA4 i Google Ads – upewnij się, że wysyłasz te same wartości (centy vs złotówki, z VAT vs bez VAT) do obu systemów.

Kampanie, landing page, tracking konwersji, GA4 i GTM w jednym procesie.
Google Ads i Analityka

Często zadawane pytania

Czy enhanced conversions zastępują zwykły conversion tracking?

Nie. To warstwa uzupełniająca, ponieważ najpierw potrzebujesz poprawnie działającego bazowego pomiaru konwersji, a dopiero potem możesz zwiększać jego dokładność przez enhanced conversions.

Czy „server-side” dla Google Ads oznacza Measurement Protocol?

Zazwyczaj nie. W kontekście Google Ads chodzi częściej o upload click conversions albo inne importy przez Google Ads API. GA4 Measurement Protocol rozwiązuje inny problem i nie jest prostym zamiennikiem natywnego pomiaru Google Ads.

Czy muszę hashować dane ręcznie?

To zależy od metody implementacji. Przy Google tag i części setupów GTM Google może wykonać hashing zgodnie z wybraną metodą. Przy API i importach offline wymagania mogą być inne. Zawsze sprawdź dokumentację dokładnie dla wybranej ścieżki wdrożenia.

Czy sam transaction_id rozwiązuje problem duplikatów?

Nie w pełni. Pomaga przy deduplikacji, ale w praktyce nadal warto pilnować, żeby event nie odpalał się wielokrotnie na stronie sukcesu, po refreshu albo przy ponownym wejściu użytkownika w ten sam widok.

Jak przechwycić GCLID, żeby nie zgubić go po nawigacji w SPA?

Odczytaj GCLID z URL przy pierwszym wejściu na stronę i zapisz go w cookie lub localStorage — np. przez hook useGclidCapture. Zapisz też timestamp przechwycenia. W ten sposób identyfikator przetrwa client-side nawigację i będzie dostępny przy konwersji, która może nastąpić kilka kroków później. W App Routerze pamiętaj, że useSearchParams wymaga obudowania komponentu w Suspense.

Co z trackingiem na iOS, skoro Apple ogranicza cookies — czy GCLID działa?

Na iOS, gdy klasyczny gclid jest niedostępny, Google przekazuje gbraid (ruch web → aplikacja) albo wbraid (aplikacja → web). Jeśli śledzisz tylko gclid, gubisz część konwersji z ruchu Apple. Przechwytuj którykolwiek z trzech identyfikatorów, a w uploadzie przez Google Ads API przekazuj dokładnie jeden — nigdy kilka naraz.

Jakie sygnały consent są potrzebne do pełnego działania Google Ads?

Google Ads wymaga trzech sygnałów: ad_storage (przechowywanie cookies reklamowych), ad_user_data (przesyłanie danych do Google) i ad_personalization (personalizacja reklam). Brak właściwych zgód dla użytkowników z EEA/UK/CH ogranicza measurement i możliwości reklamowe w tych regionach.

Tag Google Ads czy import konwersji z GA4 — co wybrać?

Dedykowany tag Google Ads przekazuje konwersje szybciej i wspiera enhanced conversions oraz atrybucję cross-device pełniej niż import z GA4, dlatego dla kluczowych konwersji (zakup, lead) jest zwykle lepszym wyborem. Import z GA4 bywa wygodny, gdy eventy już istnieją, ale dane docierają z opóźnieniem. Nigdy nie ustawiaj obu źródeł tej samej konwersji jako „primary", bo podwoisz liczbę konwersji w raportach.

Czy muszę ręcznie zapisywać GCLID, skoro gtag robi to sam?

Gtag z włączonym conversion linkerem zapisuje GCLID automatycznie w first-party cookie _gcl_aw. Ręczne przechwycenie ma sens, gdy planujesz server-side upload przez API, chcesz mieć identyfikator w swojej bazie obok zamówienia albo liczysz się z tym, że skrypt gtag bywa blokowany przez adblockery, a parametr w URL — nie.

Czy wartości konwersji powinny być z VAT czy bez?

To zależy od podejścia przyjętego w kampanii i benchmarków, do których się odwołujesz. Ważne, żeby ta sama konwencja obowiązywała konsekwentnie we wszystkich miejscach: w GA4, Google Ads i ewentualnych importach offline. Niespójność wartości utrudnia optymalizację stawek opartą na docelowym ROAS.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Google Ads Remarketing w React – dynamiczne listy odbiorców i personalizacja reklam

Jak wdrożyć remarketing Google Ads w aplikacji React? Dynamiczny remarketing ecommerce, listy odbiorców na podstawie zachowań użytkowników i integracja z listami odbiorców w GA4.

Maciej Sala

Maciej Sala

Founder StriveLab

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

Integracja Google Analytics 4 w Next.js App Router to coś więcej niż wklejenie snippetu w , 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 setup GA4 z realnymi niuansami App Router i podstawami planowania zdarzeń.

Maciej Sala

Maciej Sala

Founder StriveLab

Landing page dla Google Ads w Next.js — jak budować strony, które konwertują

Jak zbudować landing page w Next.js, która jest szybka, trafna i dobrze domyka ruch z Google Ads bez psucia atrybucji i doświadczenia użytkownika?

Maciej Sala

Maciej Sala

Founder StriveLab