Google Analytics 4
Datafly Signal delivers events to Google Analytics 4 server-to-server using the GA4 Measurement Protocol. This bypasses the need for the gtag.js client-side library entirely.
Why route Google GA4 through Datafly Signal
- Your numbers stop depending on what the browser allows. Client-side analytics loses every visit where the tag is blocked or the page unloads early. Signal delivers from the server, so Google GA4 receives the complete event stream.
- Email, phone number, first name and last name are hashed before leaving your infrastructure. Signal normalises (trim and lowercase) and applies SHA-256 inside your own deployment, so Google GA4 receives match keys rather than raw personal data.
- Client IP and user agent are preserved. Server-to-server delivery normally loses both, which weakens matching. Signal forwards the genuine visitor values as request headers so Google GA4 attributes the event to the right session.
- Click identifiers are captured and replayed. Signal stores
gbraid,gclid,wbraidfirst-party at collection time and attaches them to the conversion later, so attribution survives even when the original landing cookie has expired. - Delivery is batched, rate-limited, and retried. Signal buffers to Kafka, respects the Google GA4 rate limit, and retries on 429 or transient failure rather than dropping the event.
- Presets for Standard (page views + sessions), Retail, Travel, B2B / SaaS and Automotive. The mapping starts from a working configuration for your vertical instead of a blank field map.
- One consent decision governs every destination. Google GA4 is gated by the same consent state as the rest of your stack, so a withdrawal applies everywhere at once.
Prerequisites
Before configuring GA4 in Datafly Signal, you need a GA4 property, a web data stream, and a Measurement Protocol API secret. Follow the steps below to set these up in Google Analytics.
Step 1: Create a Google Analytics 4 Property
- Go to analytics.google.com.
- Click Admin (gear icon) in the bottom-left.
- In the Account column, click Create Account (or use an existing account).
- Enter an account name and configure your data sharing preferences.
- Click Next, then enter a property name (e.g. “My Website”).
- Set your reporting timezone and currency.
- Click Create.
Step 2: Create a Web Data Stream
- In Admin > Property > Data Streams, click Add Stream > Web.
- Enter your website URL and a stream name.
- Note the Measurement ID (format:
G-XXXXXXXXXX) — you will need this when configuring the integration in Signal.
Step 3: Create a Measurement Protocol API Secret
- In the Data Stream details page, scroll down to Measurement Protocol API secrets.
- Click Create.
- Give it a descriptive name (e.g. “Datafly Signal”).
- Copy the generated secret value — you will need this when configuring the integration in Signal.
The API secret is only shown once at creation time. Store it securely. If you lose it, you will need to create a new one.
Step 4: Disable Enhanced Measurement (Optional)
Since Signal handles all event tracking server-side, you may want to disable GA4’s Enhanced Measurement to avoid duplicate events — particularly if you previously had gtag.js installed on your site.
- In the Data Stream details page, click the Enhanced Measurement toggle.
- Disable individual events you no longer need (e.g. scrolls, outbound clicks, file downloads) or turn off Enhanced Measurement entirely.
If you are migrating from a client-side GA4 setup, disabling Enhanced Measurement prevents GA4 from collecting events that Signal is now handling server-side. You can always re-enable individual events later.
API Endpoint
POST https://www.google-analytics.com/mp/collect?measurement_id={measurement_id}&api_secret={api_secret}All events are sent as JSON payloads to the Measurement Protocol endpoint. Authentication is handled via the api_secret query parameter.
Configuration
| Field | Required | Description |
|---|---|---|
measurement_id | Yes | Your GA4 Measurement ID (e.g. G-XXXXXXXXXX). Found in GA4 Admin > Data Streams. |
api_secret | Yes | Measurement Protocol API secret. Create one in GA4 Admin > Data Streams > Measurement Protocol API secrets. |
Management UI Setup
- Go to Integrations in the sidebar.
- Open the Integration Library tab.
- Find Google GA4 or filter by Analytics.
- Click Install, choose a variant, and enter:
- Your Measurement ID from Step 2.
- Your API Secret from Step 3.
- Click Install Integration to create the integration with a ready-to-use default blueprint.
Presets
The GA4 blueprint ships with a default and five vertical presets; choose the one closest to your business:
| Preset | Description |
|---|---|
Standard (default) | Page views with full session and engagement signals. Add a vertical preset for conversions. |
Retail (ecommerce) | The GA4 recommended e-commerce events: search, lists, product views, wishlist, cart, checkout, shipping and payment, purchase, refund, promotions, sign-up and login. |
| Travel | Search, listing views, booking flow (booking_completed as purchase, booking_cancelled as refund) and leads. |
| B2B / SaaS | Sign-up, login, trials, subscriptions (purchase and refund), leads, feature use and onboarding. |
| Automotive | Vehicle search, view, compare, save and configure, brochure downloads, finance quotes, trade-in valuations, test drives, dealer enquiries and service bookings. |
| Insurance | Quote journey, leads, policy views, checkout, purchase, renewals (purchase) and cancellations (refund). |
Every preset includes the session and engagement parameters and Enhanced Conversions user_data (email, phone, first and last name as normalised SHA-256).
Management API Setup
curl -X POST http://localhost:8084/v1/admin/integration-catalog/ga4/install \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Google GA4",
"variant": "retail",
"config": {
"measurement_id": "G-XXXXXXXXXX",
"api_secret": "your_api_secret"
},
"delivery_mode": "server_side"
}'Identity
client_id
The Measurement Protocol requires a client_id to identify the user/device. Datafly Signal self-generates this value in the standard GA4 format:
{random_number}.{timestamp_seconds}For example: 1234567890.1706540000
The client_id is generated on the first event for a visitor and persisted server-side for the lifetime of the visitor. It is also stored as a first-party cookie by Datafly.js and included in all subsequent events.
user_id
If your application calls datafly.identify(userId), the user_id is forwarded to GA4 for cross-device reporting.
Enhanced Conversions (user_data)
When a user identifies via datafly.identify() with email or phone traits, Signal hashes those values (SHA-256, lowercased, trimmed) and forwards them in the GA4 user_data block:
| Trait | GA4 field | Hashing |
|---|---|---|
email | user_data.sha256_email_address | lowercase, trim, SHA-256 |
phone | user_data.sha256_phone_number | trim, SHA-256 |
Raw PII never leaves your infrastructure — hashing happens server-side before delivery.
Campaign Attribution
Signal forwards UTM and click-ID parameters automatically so GA4 attributes the event correctly:
| Source | GA4 field |
|---|---|
utm_source | source |
utm_medium | medium |
utm_campaign | campaign |
utm_term | term |
utm_content | content |
gclid | gclid |
wbraid | wbraid |
gbraid | gbraid |
Consent
GA4’s Measurement Protocol supports Google Consent Mode v2. The default blueprint maps Signal’s canonical marketing consent signal to two fields:
| Consent signal | GA4 field | Granted | Denied |
|---|---|---|---|
marketing | consent.ad_user_data | GRANTED | DENIED |
marketing | consent.ad_personalization | GRANTED | DENIED |
The ad_storage and analytics_storage fields are gtag-only and are deliberately not sent — GA4’s Measurement Protocol only accepts ad_user_data and ad_personalization.
Default consent category: analytics. Add advertising if you want events suppressed when the visitor has not consented to advertising.
Session Handling
GA4 does not create sessions for server-side hits on its own, so Datafly Signal sends the session fields with every event and delivers them to GA4 server-side. Each pipeline chooses where sessions are assigned: Browser sessions are assigned by the Datafly.js collector and travel with each event to your first-party collect endpoint; Server sessions are assigned by Signal on your first-party domain, which keeps them durable where browser storage is cleared or capped.
session_id: The start time of the current session. It stays the same while the visitor is active and rotates only after the inactivity timeout, so a continuously active visit keeps onesession_idno matter how long it runs.session_number: An incrementing count of the visitor’s sessions. With Browser sessions the collector keeps it inlocalStorage, so it restarts if the visitor clears site data or the browser removes script-written storage. With Server sessions it persists for the life of the visitor’s first-party session cookie (rolling, with the pipeline’s cookie lifetime: 400 days unless set shorter), so returning visitors keep counting up correctly.session_start/first_visit: The first event of a new session carries GA4’s_ssflag and the first event of a visitor’s first session carries_fv, each sent once, so landing pages attribute correctly. Onesession_startper session (andfirst_visiton a visitor’s first session) is also emitted for destinations that accept them.- Session timeout: 30 minutes of inactivity by default. It is a sliding window measured from the last event, not a cap on session length. Set it per pipeline (1 to 240 minutes) under Pipeline → Collector → Sessions to match your GA4 property’s session timeout.
Server-side sessions are set per pipeline under Pipeline → Collector → Sessions → Server, the default for web pipelines created in the management UI. Pipelines created before this setting existed use Browser sessions until you switch them. Visits already in progress keep their session_id when you switch.
{
"client_id": "1234567890.1706540000",
"events": [
{
"name": "page_view",
"params": {
"session_id": "1706540000",
"session_number": 3,
"engagement_time_msec": 100
}
}
]
}The engagement_time_msec parameter is required for events to appear in standard GA4 reports. Datafly Signal sets this to a minimum of 100 milliseconds to ensure events are counted as engaged.
More resilient than client-side gtag
Client-side analytics — including gtag.js — stores session state in JavaScript-set cookies and browser storage, which Safari’s Intelligent Tracking Prevention (ITP) deletes after 7 days of Safari use without a visit to your site (24 hours when the visitor arrived from an ad click carrying a parameter such as gclid), and clears entirely in private mode. Once that storage is deleted, a returning visitor is mis-counted as new: their session_number resets to 1 and first_visit re-fires, even though it’s the same person. On Safari and iOS, where a large share of traffic now sits, this quietly inflates new users and fragments sessions.
Signal computes sessions on the server and persists them in a server-set, first-party cookie on your own domain — which ITP does not delete the way it deletes JavaScript-written storage. In practice:
- returning visitors stay returning — no spurious
first_visit, session_numberkeeps counting correctly across weeks and months,- session attribution holds, so landing pages stop slipping into
(not set)from fragmented sessions.
This durability relies on your collection endpoint being a first-party subdomain (for example collect.yourdomain.com) served from your website’s own edge: a host on the same CDN or proxy as your site, or a path on your site. Safari caps server-set cookies at 7 days when the collection host is a CNAME to another company’s domain, or when its IP address does not share the first half of the IP address that served your page (the first two octets for IPv4), even with a direct A record. That genuinely first-party, server-set position is exactly what a client-side tag cannot achieve.
Migrating from gtag.js
Moving GA4 from gtag.js to Signal is a real piece of work — every tracked event has to be sent to Signal and then switched off in gtag. It is not a single config flip. This section covers how to do it safely, and the one architectural rule that determines whether you actually escape ad blocking.
Load the collector first-party, not inside GTM
This is the rule that everything else depends on. There are two separate things people call “GTM”:
- The dataLayer — a plain JavaScript array (
window.dataLayer) that your own site code pushes events into (dataLayer.push({ event: 'purchase', ... })). It is part of your site. Ad blockers do not touch it. - Google’s script — the GTM container /
gtag.js, downloaded fromgoogletagmanager.com. Its job is to read that array and send data to Google. This is what ad blockers block.
So when a visitor runs an ad blocker, the dataLayer still fills up, but Google’s script that should read it and send to GA4 is blocked, and the data is lost.
Do not install the Datafly.js collector as a tag inside GTM. If you do, the collector only loads when GTM loads — so when an ad blocker blocks GTM, your Signal tag never fires, and you have inherited exactly the blocking you were trying to escape.
Instead, add the Datafly.js script directly to your page, served from your first-party collection subdomain (e.g. collect.yourdomain.com). Because it is genuinely first-party and not a recognised tracker domain, the collector keeps running even when gtag.js is blocked. This first-party position is what makes Signal resilient to the ad blocking that costs gtag-based GA4 a meaningful share of its events.
Validate in parallel, without double-counting
GA4 does not deduplicate general events between gtag.js and the Measurement Protocol. If both send the same page_view to the same property, GA4 counts both. The only native exception is purchase events that share a transaction_id, and even that is best reconciled in BigQuery.
So during validation, do not send the same event to the same property from both sources. Point Signal at a separate GA4 property (or data stream) and compare its numbers against your existing gtag-fed production property until you trust parity. This lets you verify Signal end-to-end with zero risk to production reporting.
Day one: send your existing gtag hits through Signal (transitional)
Before touching any tagging, you can point gtag’s server_container_url at your Signal collection endpoint. gtag then sends its hits to Signal, which translates them into Signal events and delivers them through your pipeline, GA4 included. See GA4 gtag Inbound for setup and the full parameter mapping.
gtag.js itself still loads from Google on this route, so ad blockers that block it still stop the hits. It gets your data flowing through Signal immediately; it does not replace the steps below.
Accelerate the cutover: the dataLayer bridge (transitional)
Re-instrumenting every event as a datafly.track() call up front is the slowest part of the migration. The dataLayer bridge lets you go live first and re-instrument afterwards: enable it (Collector tab → Migration on-ramp, or js_builder_config.datalayer_bridge) and the collector reads the events your site already pushes and forwards them to Signal automatically.
This is a transitional on-ramp, not the destination. Leave it on only while you migrate. The end state is native datafly.track() + enhanced measurement with no dataLayer, gtag, or GTM in the chain — turn the bridge off once your events are wired natively.
What the bridge recovers — because the collector loads first-party and reads window.dataLayer directly, these keep flowing even when an ad blocker blocks GTM/gtag.js:
- Events your own site code pushes:
dataLayer.push({ event: 'add_to_cart', currency: 'GBP', value: 10 }). - Inline
gtag('event', 'purchase', { … })calls — the inline gtag stub still queues them to the dataLayer even whengtag.jsitself is blocked.
What it cannot recover: events that exist only because GTM’s own injected triggers fire (auto link-click, scroll, form, History-change). When GTM is blocked those never reach the dataLayer, so nothing can forward them. Those interactions are covered instead by datafly’s native enhanced measurement, which generates them first-party.
The bridge forwards raw events only. It preserves the native dataLayer.push (GTM/GA4 keep working), drops GTM-internal gtm.* events, and de-duplicates a page that pushes the same event in both the object and gtag forms. Configuration is optional — allow / deny lists to filter event names and remap to rename them. GA4 ecommerce-object mapping (items arrays → Signal’s ecommerce schema) is a separate, later step; until then, map ecommerce events in your blueprint as usual.
Cut over event by event
Once you trust the numbers, migrate one event at a time. For each event, add the Signal equivalent and switch off the gtag / GTM tag for that same event together, so it is never sent twice to production:
- Standard GA4 event names carry over unchanged — Signal uses a GA4-compatible schema, so
gtag('event', 'purchase', { value: 99.99 })becomesdatafly.track('purchase', { value: 99.99 }). - Datafly.js enhanced measurement covers the automatic interactions GTM used to handle — scroll depth, outbound clicks, file downloads, form start/submit, and site search — first-party, with no GTM triggers.
The destination: no third-party tag in the chain
When every event is on Signal, remove gtag.js and the GTM container entirely. Your events are now collected first-party and delivered to GA4 server-side, with nothing in the request path an ad blocker recognises. That is the end state Signal is built for: one first-party collector, no client-side vendor tags.
Presets and event mapping
Signal sends your canonical event names under their GA4 names. For every event that carries products, items is built from, in this order, exactly one of:
- your canonical
items[]array, passed through (every GA4 item parameter it carries:item_id,item_name,item_category,item_brand,item_variant,price,quantity,index, and any custom item parameter); - the legacy
products[]array, renamed to GA4’s parameters (product_idtoitem_id,nametoitem_name,categorytoitem_category,brandtoitem_brand,varianttoitem_variant,positiontoindex); - a single flat item (
item_id,item_name,price,quantityat the top level of the event).
items is not required: GA4 accepts an event without it, so a purchase without items still records its value. transaction_id is transaction_id, else the legacy order_id, and is required on events sent as purchase or refund (an event without either is not sent). value is value, else the legacy revenue: the total including tax and shipping, as the event specs define it. currency is upper-cased. search_term is search_term, else the legacy query (and, for flight_searched and hotel_searched, the destination). Events routed to /g/collect carry page and session parameters; conversion events go to /mp/collect.
Standard (page views + sessions) preset (default)
Base GA4 web stream: page_view delivered to /g/collect with full gtag session + engagement signals (cid, sid, sct, _et, _ss, _fv, gcs, campaign), plus the Measurement-Protocol root mappings (client_id, consent, hashed PII) for conversion events added by a vertical. No vertical-specific events. This is the correct default for any GA4 web property.
| Signal event | GA4 event | Fields the preset sets |
|---|---|---|
page | page_view | global fields only. |
Retail preset (ecommerce)
Full e-commerce funnel with the GA4 recommended events: page views, search, product discovery, cart, checkout, purchase, refunds, and promotions. Items come from the canonical items[], else the legacy products[], else a single flat item (item_id, item_name, price, quantity), one branch per event; legacy transaction and value names (order_id, revenue) are read as fallbacks. purchase and refund are sent only with a transaction ID; coupon, tax and shipping where GA4 takes them.
| Signal event | GA4 event | Fields the preset sets |
|---|---|---|
page | page_view | global fields only. |
search | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
view_item_list | view_item_list | items, item_list_id, item_list_name. |
select_item | select_item | items, item_list_id, item_list_name. |
view_item | view_item | items, value, currency. |
add_to_wishlist | add_to_wishlist | items, value, currency. |
add_to_cart | add_to_cart | items, value, currency. |
remove_from_cart | remove_from_cart | items, value, currency. |
view_cart | view_cart | items, value, currency. |
begin_checkout | begin_checkout | items, value, currency, coupon. |
add_shipping_info | add_shipping_info | items, value, currency, shipping_tier, coupon. |
add_payment_info | add_payment_info | items, value, currency, payment_type, coupon. |
purchase | purchase | items, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set. |
refund | refund | items, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set. |
select_promotion | select_promotion | ep.promotion_id, ep.promotion_name, ep.creative_name, ep.creative_slot. |
view_promotion | view_promotion | ep.promotion_id, ep.promotion_name, ep.creative_name, ep.creative_slot. |
sign_up | sign_up | method. |
login | login | method. |
Travel preset (travel)
Travel funnel: searches (search_term from search_term, query or the destination), listing views, booking flow and cancellations mapped to GA4 e-commerce events (booking_completed as purchase, booking_cancelled as refund). Items come from the canonical items[], else the legacy products[], else a single flat item (item_id, item_name, price, quantity), one branch per event; legacy transaction and value names (order_id, revenue) are read as fallbacks.
| Signal event | GA4 event | Fields the preset sets |
|---|---|---|
page | page_view | global fields only. |
search | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
destination_searched | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
flight_searched | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query or properties.destination is set. |
hotel_searched | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query or properties.destination is set. |
destination_viewed | view_item | items, value, currency. |
flight_viewed | view_item | items, value, currency. |
hotel_viewed | view_item | items, value, currency. |
listing_viewed | view_item | items, value, currency. |
view_item_list | view_item_list | items, item_list_id, item_list_name. |
listing_saved | add_to_wishlist | items, value, currency. |
booking_started | begin_checkout | items, value, currency, coupon. |
add_payment_info | add_payment_info | items, value, currency, payment_type, coupon. |
booking_completed | purchase | items, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set. |
booking_cancelled | refund | items, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set. |
generate_lead | generate_lead | value, currency, lead_type, quote_id. |
sign_up | sign_up | method. |
login | login | method. |
B2B / SaaS preset (saas)
SaaS lifecycle events: sign-up, login, trials, subscriptions (purchase and refund with the transaction ID), leads, feature use and onboarding. Items come from the canonical items[], else the legacy products[], else a single flat item (item_id, item_name, price, quantity), one branch per event; legacy transaction and value names (order_id, revenue) are read as fallbacks.
| Signal event | GA4 event | Fields the preset sets |
|---|---|---|
page | page_view | global fields only. |
search | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
view_item | view_item | items, value, currency. |
plan_viewed | view_item | items, value, currency. |
generate_lead | generate_lead | value, currency, lead_type, quote_id. |
demo_requested | generate_lead | value, currency, lead_type, quote_id. |
content_downloaded | generate_lead | value, currency, lead_type, quote_id. |
trial_started | begin_checkout | items, value, currency, coupon. |
subscription_started | purchase | items, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set. |
subscription_cancelled | refund | items, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set. |
feature_used | feature_used | global fields only. |
onboarding_started | onboarding_started | global fields only. |
onboarding_completed | onboarding_completed | global fields only. |
sign_up | sign_up | method. |
login | login | method. |
Automotive preset (automotive)
Vehicle marketing funnel: vehicle search/view/compare/save, brochure downloads, finance quotes, trade-in valuations, test drive bookings, dealer enquiries, and service appointments. Standard GA4 events where one fits (search, view_item, add_to_wishlist, file_download, generate_lead); custom events for lead-style actions so marketers see meaningful labels.
| Signal event | GA4 event | Fields the preset sets |
|---|---|---|
page | page_view | global fields only. |
search | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
vehicle_searched | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
vehicle_viewed | view_item | items, value, currency. |
vehicle_compared | select_item | items, item_list_id, item_list_name. |
vehicle_saved | add_to_wishlist | items, value, currency. |
configure_vehicle | view_item | items, value, currency. |
brochure_downloaded | generate_lead | value, currency, lead_type, quote_id. |
finance_quote_requested | generate_lead | value, currency, lead_type, quote_id. |
trade_in_valued | generate_lead | value, currency, lead_type, quote_id. |
test_drive_booked | generate_lead | value, currency, lead_type, quote_id. |
dealer_contacted | generate_lead | value, currency, lead_type, quote_id. |
service_appointment_booked | generate_lead | value, currency, lead_type, quote_id. |
generate_lead | generate_lead | value, currency, lead_type, quote_id. |
sign_up | sign_up | method. |
login | login | method. |
Insurance preset (insurance)
Insurance lead-gen + policy funnel. Browse/quote-intent events to /g/collect (session+engagement); generate_lead (quote submitted = the insurance conversion) and policy view/checkout/purchase to /mp/collect with items[] + value + Enhanced-Conversions hashed PII.
| Signal event | GA4 event | Fields the preset sets |
|---|---|---|
page | page_view | global fields only. |
search | search | search_term, ep.search_category. Sent only when properties.search_term or properties.query is set. |
quote_started | quote_started | global fields only. |
get_quote | generate_lead | value, currency, lead_type, quote_id. |
quote_requested | generate_lead | value, currency, lead_type, quote_id. |
quote_received | quote_received | global fields only. |
view_item_list | view_item_list | items, item_list_id, item_list_name. |
policy_renewed | purchase | items, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set. |
policy_cancelled | refund | items, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set. |
generate_lead | generate_lead | value, currency, lead_type, quote_id. |
sign_up | sign_up | method. |
login | login | method. |
view_item | view_item | items, value, currency. |
select_item | select_item | items, item_list_id, item_list_name. |
purchase | purchase | items, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set. |
begin_checkout | begin_checkout | items, value, currency, coupon. |
add_payment_info | add_payment_info | items, value, currency, payment_type, coupon. |
Example: purchase
A Retail purchase from a known visitor, as Signal sends it to the Measurement Protocol (/mp/collect):
{
"client_id": "1234567890.1790150000",
"events": [
{
"name": "purchase",
"params": {
"_uip": "203.0.113.7",
"cid": "1234567890.1790150000",
"coupon": "SAVE10",
"currency": "GBP",
"dl": "https://shop.example.co.uk/checkout?gclid=g-123",
"dr": "https://www.google.com/",
"dt": "Checkout",
"items": [
{
"currency": "GBP",
"item_brand": "BMW",
"item_category": "SUV",
"item_id": "STK-7421",
"item_name": "BMW X3 xDrive30e",
"item_variant": "M Sport",
"price": 49995,
"quantity": 1
}
],
"language": "en-GB",
"shipping": 10,
"tax": 43.33,
"transaction_id": "ORD-10042",
"ul": "en-GB",
"up": {
"anonymous_id": "df_anon_spec_0001"
},
"value": 49995
}
}
],
"ip_override": "203.0.113.7",
"timestamp_micros": 1790155800000000,
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 Version/18.0 Safari/605.1.15",
"user_data": {
"sha256_email_address": "86e0b9e56c17cc4d12387e1949b85053fbe73bc3ce5a1188713a9d300cc6133d",
"sha256_first_name": "81f8f6dde88365f3928796ec7aa53f72820b06db8664f5fe76a7eb13e24546a2",
"sha256_last_name": "799ef92a11af918e3fb741df42934f3b568ed2d93ac1df74f1b8d41a27932a6f",
"sha256_phone_number": "a8acc3a90a7b4e4dc65e93db9240ed26523050ef754d63b75b5161de76781436"
},
"user_id": "cust-1001",
"user_properties": {
"anonymous_id": {
"value": "df_anon_spec_0001"
}
}
}Parameters
Event-Level Parameters
| Parameter | Type | Description |
|---|---|---|
session_id | string | Server-generated session identifier |
session_number | integer | Incrementing session count per visitor |
engagement_time_msec | integer | Time in milliseconds the user was engaged (minimum 100) |
page_location | string | Full URL of the page |
page_title | string | Document title |
page_referrer | string | Referrer URL |
E-commerce Parameters
| Parameter | Type | Description |
|---|---|---|
transaction_id | string | Unique order/transaction identifier (transaction_id, else order_id) |
value | number | Total monetary value, including tax and shipping (value, else revenue) |
currency | string | ISO 4217 currency code, upper-cased (e.g. GBP, EUR) |
tax, shipping, coupon | number / string | On purchase and refund; coupon also on checkout, shipping and payment steps |
items | array | Array of item objects (see above for the three sources) |
Item Parameters
| Parameter | Type | Description |
|---|---|---|
item_id | string | Product SKU or identifier |
item_name | string | Product name |
price | number | Unit price |
quantity | integer | Number of units |
item_category | string | Product category |
item_brand | string | Product brand |
Limitations
- Google Signals: Demographic and interest data from Google Signals is not available with server-side Measurement Protocol. These features require the client-side
gtag.jslibrary. - Automatic events: GA4 automatic events (e.g.
scroll,outbound_click,file_download) are not collected unless you explicitly track them with Datafly.js. - Real-time reports: Events sent via the Measurement Protocol may take a few seconds longer to appear in GA4 real-time reports compared to client-side collection.
- BigQuery export: Events sent via the Measurement Protocol are included in BigQuery exports but may have limited attribution data.
- Parameter limits: GA4 allows a maximum of 25 event parameters per event and 25 user properties per project.
Verify it’s working
- In the Signal Management UI, open your integration and watch the Live Events stream as you trigger events on your site.
- In GA4, open Reports > Realtime. Server-side events appear in Realtime within ~30 seconds.
- Open Admin > DebugView (only works while
debug_modeis on for a session, or while the integration hasdebug_mode: trueconfigured) to inspect individual event payloads. - After 24-48 hours, check Reports > Engagement > Events to confirm event volumes are normal.
Troubleshooting
| Problem | Solution |
|---|---|
| Events not appearing | Confirm measurement_id and api_secret are for the same Data Stream. The Measurement Protocol silently accepts mismatched pairs. |
| Events accepted but missing from reports | Set engagement_time_msec to at least 100 — without it, GA4 treats events as non-engaged and may hide them. Signal sets this automatically. |
| Realtime works but standard reports show nothing | Check the Data Stream’s filters and internal_traffic settings — these silently exclude events. |
| User Acquisition shows no data | Ensure gclid, wbraid, gbraid, or UTM parameters are reaching Datafly.js. Signal forwards them automatically when present on the landing URL. |
| Enhanced Conversions match rate is low | Call datafly.identify(userId, { email, phone }) whenever you have the data — at login, signup, checkout, or form submit. |
Debugging
Use the GA4 debug endpoint to validate payloads without sending data to production:
POST https://www.google-analytics.com/debug/mp/collect?measurement_id={measurement_id}&api_secret={api_secret}The debug endpoint returns validation messages for each event:
{
"validationMessages": [
{
"fieldPath": "events[0].params.value",
"description": "Expected type NUMBER, got STRING.",
"validationCode": "VALUE_INVALID"
}
]
}To enable debug mode for your integration, set debug_mode: true in the integration configuration. This routes events through the debug endpoint and logs the validation responses.
{
"config": {
"measurement_id": "G-XXXXXXXXXX",
"api_secret": "your_api_secret",
"debug_mode": true
}
}Events sent to the debug endpoint are not recorded in your GA4 property. Disable debug_mode before going to production.
Rate Limits
Google’s Measurement Protocol does not publish strict rate limits, but the following defaults are used:
| Setting | Default |
|---|---|
rate_limit_rps | 100 |
rate_limit_burst | 200 |
If you experience 429 Too Many Requests responses, reduce rate_limit_rps or contact Google to discuss higher quotas.
Blueprint
The GA4 blueprint version is 2.2.0 (ga). It ships with four variants (Retail, Travel, B2B / SaaS, Automotive), Consent Mode v2, Enhanced Conversions, and the 16 GA4 recommended e-commerce events.
See also
- Meta Conversions API — pair with GA4 for paid-media measurement.
- Google Ads Offline Conversions — closes the loop on Google paid-search attribution.
- Cross-Platform Deduplication — run GA4 alongside gtag.js during migration.