IntegrationsAnalyticsGoogle Analytics 4

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, wbraid first-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

  1. Go to analytics.google.com.
  2. Click Admin (gear icon) in the bottom-left.
  3. In the Account column, click Create Account (or use an existing account).
  4. Enter an account name and configure your data sharing preferences.
  5. Click Next, then enter a property name (e.g. “My Website”).
  6. Set your reporting timezone and currency.
  7. Click Create.

Step 2: Create a Web Data Stream

  1. In Admin > Property > Data Streams, click Add Stream > Web.
  2. Enter your website URL and a stream name.
  3. 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

  1. In the Data Stream details page, scroll down to Measurement Protocol API secrets.
  2. Click Create.
  3. Give it a descriptive name (e.g. “Datafly Signal”).
  4. 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.

  1. In the Data Stream details page, click the Enhanced Measurement toggle.
  2. 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

FieldRequiredDescription
measurement_idYesYour GA4 Measurement ID (e.g. G-XXXXXXXXXX). Found in GA4 Admin > Data Streams.
api_secretYesMeasurement Protocol API secret. Create one in GA4 Admin > Data Streams > Measurement Protocol API secrets.

Management UI Setup

  1. Go to Integrations in the sidebar.
  2. Open the Integration Library tab.
  3. Find Google GA4 or filter by Analytics.
  4. Click Install, choose a variant, and enter:
    • Your Measurement ID from Step 2.
    • Your API Secret from Step 3.
  5. 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:

PresetDescription
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.
TravelSearch, listing views, booking flow (booking_completed as purchase, booking_cancelled as refund) and leads.
B2B / SaaSSign-up, login, trials, subscriptions (purchase and refund), leads, feature use and onboarding.
AutomotiveVehicle search, view, compare, save and configure, brochure downloads, finance quotes, trade-in valuations, test drives, dealer enquiries and service bookings.
InsuranceQuote 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:

TraitGA4 fieldHashing
emailuser_data.sha256_email_addresslowercase, trim, SHA-256
phoneuser_data.sha256_phone_numbertrim, 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:

SourceGA4 field
utm_sourcesource
utm_mediummedium
utm_campaigncampaign
utm_termterm
utm_contentcontent
gclidgclid
wbraidwbraid
gbraidgbraid

GA4’s Measurement Protocol supports Google Consent Mode v2. The default blueprint maps Signal’s canonical marketing consent signal to two fields:

Consent signalGA4 fieldGrantedDenied
marketingconsent.ad_user_dataGRANTEDDENIED
marketingconsent.ad_personalizationGRANTEDDENIED

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 one session_id no matter how long it runs.
  • session_number: An incrementing count of the visitor’s sessions. With Browser sessions the collector keeps it in localStorage, 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 _ss flag and the first event of a visitor’s first session carries _fv, each sent once, so landing pages attribute correctly. One session_start per session (and first_visit on 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_number keeps 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 from googletagmanager.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 when gtag.js itself 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 }) becomes datafly.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:

  1. 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);
  2. the legacy products[] array, renamed to GA4’s parameters (product_id to item_id, name to item_name, category to item_category, brand to item_brand, variant to item_variant, position to index);
  3. a single flat item (item_id, item_name, price, quantity at 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 eventGA4 eventFields the preset sets
pagepage_viewglobal 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 eventGA4 eventFields the preset sets
pagepage_viewglobal fields only.
searchsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
view_item_listview_item_listitems, item_list_id, item_list_name.
select_itemselect_itemitems, item_list_id, item_list_name.
view_itemview_itemitems, value, currency.
add_to_wishlistadd_to_wishlistitems, value, currency.
add_to_cartadd_to_cartitems, value, currency.
remove_from_cartremove_from_cartitems, value, currency.
view_cartview_cartitems, value, currency.
begin_checkoutbegin_checkoutitems, value, currency, coupon.
add_shipping_infoadd_shipping_infoitems, value, currency, shipping_tier, coupon.
add_payment_infoadd_payment_infoitems, value, currency, payment_type, coupon.
purchasepurchaseitems, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set.
refundrefunditems, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set.
select_promotionselect_promotionep.promotion_id, ep.promotion_name, ep.creative_name, ep.creative_slot.
view_promotionview_promotionep.promotion_id, ep.promotion_name, ep.creative_name, ep.creative_slot.
sign_upsign_upmethod.
loginloginmethod.

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 eventGA4 eventFields the preset sets
pagepage_viewglobal fields only.
searchsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
destination_searchedsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
flight_searchedsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query or properties.destination is set.
hotel_searchedsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query or properties.destination is set.
destination_viewedview_itemitems, value, currency.
flight_viewedview_itemitems, value, currency.
hotel_viewedview_itemitems, value, currency.
listing_viewedview_itemitems, value, currency.
view_item_listview_item_listitems, item_list_id, item_list_name.
listing_savedadd_to_wishlistitems, value, currency.
booking_startedbegin_checkoutitems, value, currency, coupon.
add_payment_infoadd_payment_infoitems, value, currency, payment_type, coupon.
booking_completedpurchaseitems, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set.
booking_cancelledrefunditems, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set.
generate_leadgenerate_leadvalue, currency, lead_type, quote_id.
sign_upsign_upmethod.
loginloginmethod.

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 eventGA4 eventFields the preset sets
pagepage_viewglobal fields only.
searchsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
view_itemview_itemitems, value, currency.
plan_viewedview_itemitems, value, currency.
generate_leadgenerate_leadvalue, currency, lead_type, quote_id.
demo_requestedgenerate_leadvalue, currency, lead_type, quote_id.
content_downloadedgenerate_leadvalue, currency, lead_type, quote_id.
trial_startedbegin_checkoutitems, value, currency, coupon.
subscription_startedpurchaseitems, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set.
subscription_cancelledrefunditems, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set.
feature_usedfeature_usedglobal fields only.
onboarding_startedonboarding_startedglobal fields only.
onboarding_completedonboarding_completedglobal fields only.
sign_upsign_upmethod.
loginloginmethod.

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 eventGA4 eventFields the preset sets
pagepage_viewglobal fields only.
searchsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
vehicle_searchedsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
vehicle_viewedview_itemitems, value, currency.
vehicle_comparedselect_itemitems, item_list_id, item_list_name.
vehicle_savedadd_to_wishlistitems, value, currency.
configure_vehicleview_itemitems, value, currency.
brochure_downloadedgenerate_leadvalue, currency, lead_type, quote_id.
finance_quote_requestedgenerate_leadvalue, currency, lead_type, quote_id.
trade_in_valuedgenerate_leadvalue, currency, lead_type, quote_id.
test_drive_bookedgenerate_leadvalue, currency, lead_type, quote_id.
dealer_contactedgenerate_leadvalue, currency, lead_type, quote_id.
service_appointment_bookedgenerate_leadvalue, currency, lead_type, quote_id.
generate_leadgenerate_leadvalue, currency, lead_type, quote_id.
sign_upsign_upmethod.
loginloginmethod.

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 eventGA4 eventFields the preset sets
pagepage_viewglobal fields only.
searchsearchsearch_term, ep.search_category. Sent only when properties.search_term or properties.query is set.
quote_startedquote_startedglobal fields only.
get_quotegenerate_leadvalue, currency, lead_type, quote_id.
quote_requestedgenerate_leadvalue, currency, lead_type, quote_id.
quote_receivedquote_receivedglobal fields only.
view_item_listview_item_listitems, item_list_id, item_list_name.
policy_renewedpurchaseitems, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set.
policy_cancelledrefunditems, transaction_id, value, currency, coupon, tax, shipping. Sent only when properties.transaction_id or properties.order_id is set.
generate_leadgenerate_leadvalue, currency, lead_type, quote_id.
sign_upsign_upmethod.
loginloginmethod.
view_itemview_itemitems, value, currency.
select_itemselect_itemitems, item_list_id, item_list_name.
purchasepurchaseitems, transaction_id, value, currency, tax, shipping, coupon. Sent only when properties.transaction_id or properties.order_id is set.
begin_checkoutbegin_checkoutitems, value, currency, coupon.
add_payment_infoadd_payment_infoitems, 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

ParameterTypeDescription
session_idstringServer-generated session identifier
session_numberintegerIncrementing session count per visitor
engagement_time_msecintegerTime in milliseconds the user was engaged (minimum 100)
page_locationstringFull URL of the page
page_titlestringDocument title
page_referrerstringReferrer URL

E-commerce Parameters

ParameterTypeDescription
transaction_idstringUnique order/transaction identifier (transaction_id, else order_id)
valuenumberTotal monetary value, including tax and shipping (value, else revenue)
currencystringISO 4217 currency code, upper-cased (e.g. GBP, EUR)
tax, shipping, couponnumber / stringOn purchase and refund; coupon also on checkout, shipping and payment steps
itemsarrayArray of item objects (see above for the three sources)

Item Parameters

ParameterTypeDescription
item_idstringProduct SKU or identifier
item_namestringProduct name
pricenumberUnit price
quantityintegerNumber of units
item_categorystringProduct category
item_brandstringProduct 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.js library.
  • 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

  1. In the Signal Management UI, open your integration and watch the Live Events stream as you trigger events on your site.
  2. In GA4, open Reports > Realtime. Server-side events appear in Realtime within ~30 seconds.
  3. Open Admin > DebugView (only works while debug_mode is on for a session, or while the integration has debug_mode: true configured) to inspect individual event payloads.
  4. After 24-48 hours, check Reports > Engagement > Events to confirm event volumes are normal.

Troubleshooting

ProblemSolution
Events not appearingConfirm measurement_id and api_secret are for the same Data Stream. The Measurement Protocol silently accepts mismatched pairs.
Events accepted but missing from reportsSet 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 nothingCheck the Data Stream’s filters and internal_traffic settings — these silently exclude events.
User Acquisition shows no dataEnsure 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 lowCall 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:

SettingDefault
rate_limit_rps100
rate_limit_burst200

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