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.

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.

Variants

The GA4 blueprint ships with four pre-tuned variants — choose the one closest to your business:

VariantDescription
RetailFull e-commerce funnel with all 16 GA4 recommended events: page views, product discovery, cart, checkout, purchase, refunds, and promotions.
TravelTravel and hospitality funnel: search, listing views, booking flow, cancellations, and promotions mapped to GA4 e-commerce events.
B2B / SaaSSaaS lifecycle events: sign-up, login, trials, subscriptions, leads, and content engagement mapped to GA4.
AutomotiveVehicle marketing funnel: vehicle search/view/compare/save, brochure downloads, finance quotes, trade-in valuations, test drive bookings, dealer enquiries, and service appointments.

Every variant includes session_id, engagement_time_msec, and SHA-256 hashed email/phone for Enhanced Conversions.

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’s Measurement Protocol does not automatically create sessions. Datafly Signal manages sessions server-side — and because it runs on your own first-party domain, it does so more durably than client-side gtag.js can.

  • session_id: A numeric identifier set at the start of each session and stable for its entire duration. It 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 counter tracking how many sessions the visitor has had. It persists for the life of the visitor’s first-party cookie (a rolling 400 days), so returning visitors keep counting up correctly.
  • session_start / first_visit: Signal emits GA4’s _ss and _fv signals exactly once — on the first event of a new session, and the first session ever, respectively — so landing pages attribute correctly and new-vs-returning stays accurate.
  • Session timeout: 30 minutes of inactivity — a sliding window measured from the last event, not a cap on session length.
{
  "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 browser storage that Safari’s Intelligent Tracking Prevention (ITP) caps at 7 days and clears entirely in private mode. Once that window lapses, 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 cap the way it caps 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) that points directly at your Signal deployment — the default for Signal installs. 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.

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.

Event Mapping

Datafly Signal maps standard event names to their GA4 equivalents:

Datafly EventGA4 EventNotes
pagepage_viewIncludes page_location, page_title, page_referrer
Products SearchedsearchRequires search_term
Product List Viewedview_item_listRequires items array, item_list_name
Product Clickedselect_itemRequires items array
Product Viewedview_itemRequires items array
Product Added to Wishlistadd_to_wishlistRequires items array
Product Addedadd_to_cartRequires items array
Product Removedremove_from_cartRequires items array
Cart Viewedview_cartIncludes value, currency, items
Checkout Startedbegin_checkoutRequires items array, value, currency
Shipping Info Enteredadd_shipping_infoIncludes shipping_tier
Payment Info Enteredadd_payment_infoIncludes payment_type
Order CompletedpurchaseRequires transaction_id, value, currency
Order RefundedrefundRequires transaction_id
Promotion Viewedview_promotionIncludes promotion_id, promotion_name, creative_name, creative_slot
Promotion Clickedselect_promotionIncludes promotion_id, promotion_name, creative_name, creative_slot
Custom eventsPassed throughAny datafly.track("event_name") is sent as-is

Example: Purchase Event

Datafly.js call:

datafly.track("Order Completed", {
  order_id: "ORD-001",
  total: 129.99,
  currency: "USD",
  products: [
    { product_id: "SKU-A", name: "Widget", price: 49.99, quantity: 2 },
    { product_id: "SKU-B", name: "Gadget", price: 30.01, quantity: 1 }
  ]
});

GA4 Measurement Protocol payload sent by Datafly:

{
  "client_id": "1234567890.1706540000",
  "events": [
    {
      "name": "purchase",
      "params": {
        "transaction_id": "ORD-001",
        "value": 129.99,
        "currency": "USD",
        "session_id": "1706540000",
        "session_number": 3,
        "engagement_time_msec": 100,
        "items": [
          {
            "item_id": "SKU-A",
            "item_name": "Widget",
            "price": 49.99,
            "quantity": 2
          },
          {
            "item_id": "SKU-B",
            "item_name": "Gadget",
            "price": 30.01,
            "quantity": 1
          }
        ]
      }
    }
  ]
}

Example: Page View

GA4 Measurement Protocol payload:

{
  "client_id": "1234567890.1706540000",
  "events": [
    {
      "name": "page_view",
      "params": {
        "page_location": "https://example.com/products/widgets",
        "page_title": "Widgets | Example Store",
        "page_referrer": "https://www.google.com/",
        "session_id": "1706540000",
        "session_number": 3,
        "engagement_time_msec": 100
      }
    }
  ]
}

Custom Events

Any event not in the mapping table is sent to GA4 with its original name. All event properties are forwarded as GA4 event parameters.

datafly.track("newsletter_signup", { method: "footer_form" });
{
  "client_id": "1234567890.1706540000",
  "events": [
    {
      "name": "newsletter_signup",
      "params": {
        "method": "footer_form",
        "session_id": "1706540000",
        "session_number": 1,
        "engagement_time_msec": 100
      }
    }
  ]
}

GA4 event names must be 40 characters or fewer, use only letters, numbers, and underscores, and must start with a letter. Datafly Signal automatically sanitises event names to comply with these requirements.

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
valuenumberTotal monetary value
currencystringISO 4217 currency code (e.g. USD, EUR)
itemsarrayArray of item objects

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