hello@newnorth.nl+31 (0) 85 401 31 62
/Journal

BigQuery Event Tag: GA4-events uit je server-container in je eigen BigQuery

GidsServer-side2026.09.22
Freek Kampen
Freek KampenMede-oprichter, New North Digital

Een server-side GTM-template die elk GA4-event naar je eigen BigQuery-tabel schrijft, in bijna dezelfde vorm als de GA4-export. Zonder sampling, zonder dagelijkse exportlimiet en binnen seconden bevraagbaar.

Wat het doet

De BigQuery Event Tag schrijft elk GA4-event dat je server-container binnenkrijgt weg naar een BigQuery-tabel in je eigen project. De kolommen volgen waar mogelijk de native GA4-export, dus query's die je uit GA4 kent werken grotendeels ook hier.

Wat je ermee wint tegenover die native export:

  • Geen sampling en geen dagelijkse limiet. De gratis GA4-export stopt bij een miljoen events per dag; deze tag streamt alles.
  • Binnen seconden bevraagbaar, in plaats van wachten op de export van morgen.
  • Je bepaalt zelf wat erin komt. Parameters toevoegen, uitsluiten of overschrijven gebeurt in de tag.

De template staat in de gallery als BigQuery Event Tag by New North en de broncode op GitHub.

Hoe het werkt

Je site of app stuurt GA4-hits naar je server-container. De GA4-client in die container zet elke hit om in event data: elke parameter wordt een sleutel. Deze tag leest die event data, bouwt er één BigQuery-rij van en streamt die naar de tabel die je opgeeft.

  • Bron: je website of app, via een GA4-tag met een server-container-URL of via de Firebase SDK.
  • Server-container: de GA4-client ontleedt de hit.
  • Deze tag: vertaalt de event data naar een GA4-achtige rij en voegt die in.
  • Bestemming: je eigen tabel, per dag gepartitioneerd.

Nieuwe parameters pakt de tag vanzelf op. Zet je er één bij op de GA4-tag in je webcontainer, dan staat hij in event_params zonder dat je de server-container aanraakt. De enige uitzondering is de allowlist-modus, verderop.

Voordat je begint

  • Een draaiende server-side GTM-container met een GA4-client die verkeer van je webtags binnenkrijgt.
  • Een Google Cloud-project met BigQuery aan. Dat mag hetzelfde project zijn als waar de container draait, of een ander.
  • Rechten om in dat project een dataset te maken en IAM-rollen uit te delen.

Draai je op Cloud Run, zet CPU-throttling uit. De tag antwoordt eerst de browser en voegt daarna op de achtergrond in, zodat de responstijd laag blijft. Dat achtergrondwerk rondt alleen betrouwbaar af als de CPU altijd toegewezen blijft. Zonder die instelling blijven inserts bij weinig verkeer hangen of verdwijnen ze.

gcloud run services update <service> --region <region> --no-cpu-throttling

Installeren in vijf stappen

1. Maak de tabel. Maak een dataset in de regio waar de data moet staan, en daarna de tabel uit het meegeleverde schema. Partitioneer op event_date en cluster op event_name: bijna elke query filtert daarop en het houdt de kosten laag.

bq --location=europe-west4 mk --dataset my-project:sgtm

bq mk --table \
  --schema bigquery-event-tag-schema.json \
  --time_partitioning_type DAY \
  --time_partitioning_field event_date \
  --clustering_fields event_name \
  my-project:sgtm.events

Eigen kolommen toevoegen mag. De tag schrijft alleen wat hij kent, en BigQuery negeert velden die de tabel niet heeft. Een insert breekt er dus nooit op, maar een veld dat de tag stuurt verdwijnt stil tot de kolom bestaat.

2. Geef de container toegang. De tag schrijft onder het serviceaccount waar je container mee draait. Geef dat account BigQuery Data Editor op de dataset. Meer is niet nodig: geen queryrechten en geen Editor op projectniveau.

bq add-iam-policy-binding \
  --member=serviceAccount:<runtime-sa> \
  --role=roles/bigquery.dataEditor \
  my-project:sgtm

3. Importeer de template. In de server-container: Templates, Tag Templates, Search Gallery, en voeg BigQuery Event Tag by New North toe. Wil je een nieuwere versie dan in de gallery staat, importeer dan template.tpl uit de repository.

4. Richt de tag in. Alleen project, dataset en tabel zijn verplicht; de rest heeft bruikbare standaardwaarden. Zet Event Name op de ingebouwde variabele {{Event Name}}, en User ID op een Event Data-variabele als je een eigen ingelogde gebruiker meestuurt. Vuur de tag op een custom trigger waar Client Name equals de naam van je GA4-client. Events die je nooit in BigQuery wilt, zoals advertentie-impressies bij hoog volume, sluit je in diezelfde trigger uit.

5. Test en publiceer. Open Preview in de server-container, klik een request aan en controleer of de tag onder Tags Fired staat. Met Log to console zie je de exacte rij. Gestreamde rijen zijn binnen seconden te bevragen:

SELECT event_timestamp, event_name, user_pseudo_id
FROM `my-project.sgtm.events`
WHERE event_date = CURRENT_DATE()
ORDER BY event_timestamp DESC
LIMIT 20

Publiceer daarna en kijk in je Cloud Run-logs of er BigQuery insert FAILED voorbijkomt. Die regel betekent dat een insert twee keer mislukte, en noemt de tabel en het event. Zet er een log-based alert op, dan weet je het voordat er een gat in je data zit.

Wat er in een rij staat

De kolomnamen volgen de GA4-export waar dat kan.

  • event_date, event_timestamp: partitiedatum en tijd in microseconden. De clienttijd wordt afgetopt op servertijd, zodat een verkeerd ingestelde telefoon niet in de toekomst schrijft.
  • event_name, user_id, user_pseudo_id, ga_session_id, ga_session_number, stream_id. Sessievelden blijven leeg als de hit ze niet heeft; de tag verzint er nooit een.
  • source, medium, campaign en collected_traffic_source: de attributie van dit event, niet van de sessie, uit UTM's, klik-ID's en de referrer.
  • platform, device_category, app_info, device en geo.
  • event_params en user_properties: elke parameter als sleutel met een getypeerde waarde.
  • privacy_info: de consent-status van de hit, met ad_storage, analytics_storage, ad_user_data en ad_personalization.

Elke sleutel heeft een value-record met string_value, int_value en float_value. De tag vult het veld dat bij de binnenkomende waarde past. Komt een parameter op de ene pagina binnen als "5" en op de andere als 5, dan landt hij in twee verschillende velden. Houd het type consistent bij de bron, of lees hem uit met COALESCE.

Parameters sturen

All is de standaard: alles wat in de event data zit wordt geschreven, behalve de interne velden van GA4. Wat je niet wilt opslaan, zet je in Parameters to Exclude. Een veelgebruikte is ip_override.

Allowlist schrijft alleen de sleutels die je opgeeft. Bij hoog volume scheelt dat in insert- en opslagkosten. De keerzijde: een nieuwe parameter van de website wordt genegeerd tot iemand hem toevoegt. Doe dit alleen als die lijst een eigenaar heeft.

Parameters to Add / Edit wordt altijd geschreven, in welke modus dan ook. Handig voor waarden die de site niet stuurt, zoals een site- of omgevingslabel, of om een parameter te overschrijven met een opgeschoonde versie.

Een nieuwe parameter voeg je toe aan de GA4-tag in je webcontainer. In All-modus is dat de enige stap. Een dataLayer-waarde die niet op die GA4-tag staat, verlaat de browser niet en kan dus ook niet in BigQuery komen.

Consent en privacy

De tag blokkeert zelf niet op consent. Hij legt de consent-status van elke hit vast in privacy_info, zodat je bij het bevragen kunt filteren. Mogen bepaalde hits volgens je grondslag helemaal niet opgeslagen worden, blokkeer ze dan in de trigger.

  • Pagina-URL's worden compleet opgeslagen, inclusief querystring. Daar kunnen e-mailadressen of order-ID's in staan. Sluit page_location en page_referrer uit, of schoon ze op met Parameters to Add / Edit.
  • De user agent staat in device als web_info.user_agent. Uit te sluiten onder Device Properties.
  • IP-adressen komen in event_params terecht als je webtags ip_override meesturen. Sluit die sleutel uit tenzij je hem nodig hebt.

Draait je container achter Cloudflare of een andere proxy, dan ziet de geo-lookup van Google het datacenter en niet de bezoeker. De tag neemt het land dan over uit de country-header van de proxy. Wijkt dat land af van wat Google zegt, dan schrijft hij alleen het land, omdat regio en stad het datacenter zouden beschrijven. Sluit regio, stad en metro uit onder Geo Properties om de tabel eerlijk te houden.

Query's en problemen oplossen

Filter altijd op event_date. Dat is de partitiekolom en die bepaalt hoeveel data BigQuery scant, en dus wat je betaalt.

SELECT
  (SELECT value.string_value FROM UNNEST(event_params) WHERE key = 'page_location') AS page,
  COUNT(*) AS page_views
FROM `my-project.sgtm.events`
WHERE event_date BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 7 DAY) AND DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
  AND event_name = 'page_view'
GROUP BY page
ORDER BY page_views DESC

Alleen hits met toestemming voor analytics:

WHERE event_date = DATE_SUB(CURRENT_DATE(), INTERVAL 1 DAY)
  AND (SELECT value.string_value FROM UNNEST(privacy_info)
       WHERE key = 'analytics_storage') = 'Yes'

Gaat er iets mis, dan is het meestal een van deze vijf:

  • Helemaal geen rijen. Het serviceaccount mist BigQuery Data Editor op de dataset, of er zit een typefout in project, dataset of tabel. Zoek in de Cloud Run-logs op BigQuery insert FAILED.
  • Wel rijen in preview, gaten in productie. CPU-throttling staat nog aan op Cloud Run.
  • Een nieuwe parameter ontbreekt. Hij staat niet op de GA4-tag in de webcontainer, of allowlist-modus staat aan en de sleutel mist in de lijst.
  • Een veld blijft leeg. De tabel heeft er geen kolom voor. BigQuery laat onbekende velden zonder foutmelding vallen.
  • Een parameter is half gevuld. Hij komt als tekst en als getal binnen. Lees hem met COALESCE en repareer het type bij de bron.

Vragen of een bug? Open een issue op GitHub of mail hello@newnorth.nl.

Wil je hierover doorpraten?

Praten over jouw data?

Vertel ons over je stack, je doelen en de data die je nu mist.

Duurt 1 minuut