Skip to main content
Ask your AI

GA4

Server Source code Package

Decoder transformer that turns Google Analytics 4 Measurement Protocol v2 hits (/g/collect, /mp/collect) into walkerOS events. Drop it in a server source's before chain to ingest existing gtag/Google Tag traffic without changing the front-end. One HTTP request can carry many GA4 events; the transformer returns one walkerOS event per GA4 event in the hit.

This is the v1 release (0.1.0) with an explicit scope: server-side decoding via source-express, GA4 v2 only, replace-not-merge mapping semantics. See Caveats and Roadmap for the boundaries.

Installation​

npm install @walkeros/transformer-ga4

Wire it up​

The transformer reads ctx.ingest.url (required) and ctx.ingest.body (optional) from the source it sits in front of. The recommended pairing is @walkeros/server-source-express:

{
"version": 4,
"flows": {
"default": {
"config": { "platform": "server" },
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"ingest": {
"map": {
"url": { "key": "url" },
"path": { "key": "path" },
"method": { "key": "method" },
"body": { "key": "body" }
}
}
},
"before": "ga4"
}
},
"transformers": {
"ga4": { "package": "@walkeros/transformer-ga4" }
},
"destinations": {
"log": { "package": "@walkeros/destination-demo" }
}
}
}
}

Ingest contract​

The transformer expects the source to populate ctx.ingest with these keys:

KeyTypeRequiredNotes
urlstringyesFull request URL including the query string.
pathstringyesRequest path, drives the /g/collect before match.
bodystringnoRaw POST body. Multi-event batches are \n-separated lines.

config.ingest must use the map operator with direct req field paths (no req. prefix), as shown above. A bare object like { "url": "req.url" } is silently inert: without an operator the source returns req itself, ctx.ingest stays empty, and raw GA4 params reach the collector as a nameless event.

If url is missing or not a string the transformer drops the event silently. If body is JSON-parsed by the source before reaching the transformer, pass the original raw string through or skip the transformer.

Configuration​

This transformer uses the standard transformer config wrapper (consent, data, env, id, ...). For the shared fields see transformer configuration. Package-specific fields live under config.settings and are listed below.

Settings​

This package has no package-specific settings.

Mapping​

This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.

Examples

Add to cart

A GA4 add_to_cart hit decoded to a walkerOS product add event with currency and value.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=add_to_cart&ep.currency=EUR&epn.value=129.99"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "product add",
  "entity": "product",
  "action": "add",
  "data": {
    "currency": "EUR",
    "value": 129.99
  }
}

Batched POST (fan-out)

A single POST request carrying two newline-separated events fans out into two walkerOS events.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000",
  "body": "en=add_to_cart&ep.currency=EUR&epn.value=19.99\nen=add_to_cart&ep.currency=EUR&epn.value=29.99"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "product add",
  "entity": "product",
  "action": "add",
  "data": {
    "currency": "EUR",
    "value": 19.99
  }
};

return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "product add",
  "entity": "product",
  "action": "add",
  "data": {
    "currency": "EUR",
    "value": 29.99
  }
}

Begin checkout

A GA4 begin_checkout hit decoded to a walkerOS order start event with currency, value, and coupon.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=begin_checkout&ep.currency=EUR&epn.value=149.97&ep.coupon=WELCOME10"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "order start",
  "entity": "order",
  "action": "start",
  "data": {
    "currency": "EUR",
    "value": 149.97,
    "coupon": "WELCOME10"
  }
}

Consent denied (gcs=G100)

A page_view hit with gcs=G100 still maps, with consent.{marketing,analytics} both false on the resulting event.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&gcs=G100&en=page_view&dl=https%3A%2F%2Fx&dt=X"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {
    "marketing": false,
    "analytics": false
  },
  "name": "page view",
  "entity": "page",
  "action": "view",
  "data": {
    "id": "https://x",
    "title": "X"
  }
}

Custom event (* fallback)

Unknown GA4 event names hit the * fallback rule and surface as a ga4 track event carrying the original name.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=newsletter_subscribe&ep.source=footer"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "ga4 track",
  "entity": "ga4",
  "action": "track",
  "data": {
    "event_name": "newsletter_subscribe"
  }
}

Login

A GA4 login hit decoded to a walkerOS session login event with the auth method.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=login&ep.method=google"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "session login",
  "entity": "session",
  "action": "login",
  "data": {
    "method": "google"
  }
}

Page view

A standard GA4 page_view hit decoded to a walkerOS page view with id, title, and referrer.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=page_view&dl=https%3A%2F%2Fshop.example.com%2Fproducts%2Fsku-123&dt=Trail%20Runner%20Pro&dr=https%3A%2F%2Fshop.example.com%2F"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "page view",
  "entity": "page",
  "action": "view",
  "data": {
    "id": "https://shop.example.com/products/sku-123",
    "title": "Trail Runner Pro",
    "referrer": "https://shop.example.com/"
  }
}

Purchase (canary)

A GA4 purchase hit decoded to a walkerOS order complete event with id, currency, total, tax, shipping, and coupon.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=purchase&ep.transaction_id=T-9001&ep.currency=EUR&epn.value=149.97&epn.tax=23.97&epn.shipping=4.95&ep.coupon=WELCOME10"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "order complete",
  "entity": "order",
  "action": "complete",
  "data": {
    "id": "T-9001",
    "currency": "EUR",
    "total": 149.97,
    "tax": 23.97,
    "shipping": 4.95,
    "coupon": "WELCOME10"
  }
}

Scroll

A GA4 scroll hit decoded to a walkerOS page scroll event with the percent_scrolled value.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=scroll&epn.percent_scrolled=90"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "page scroll",
  "entity": "page",
  "action": "scroll",
  "data": {
    "percent": 90
  }
}

Search

A GA4 search hit decoded to a walkerOS search submit event carrying the search term.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=search&ep.search_term=trail%20runner"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "search submit",
  "entity": "search",
  "action": "submit",
  "data": {
    "term": "trail runner"
  }
}

user_engagement (ignored)

Auto-fired GA4 user_engagement events are dropped by default — the transformer returns false.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=user_engagement&_et=1500"
}
Out
return false

View item

A GA4 view_item hit decoded to a walkerOS product view event with currency and value.

Event
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&cid=cid-1&sid=1700000000&en=view_item&ep.currency=EUR&epn.value=129.99"
}
Out
return {
  "id": "p1",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4"
  },
  "consent": {},
  "name": "product view",
  "entity": "product",
  "action": "view",
  "data": {
    "currency": "EUR",
    "value": 129.99
  }
}

Default mappings​

transformer-ga4 ships with default mappings for 33 standard GA4 event names. Out of the box, you get pageviews, ecommerce, list/promotion, engagement, and auth events mapped to walkerOS's entity-action naming.

Page / scroll / click​

GA4 (en)walkerOS (name)Fields
page_viewpage viewid, title, referrer
scrollpage scrollpercent
clicklink clickurl, domain, outbound
file_downloadfile downloadname, extension, url

Ecommerce​

GA4 (en)walkerOS (name)Fields
view_itemproduct viewcurrency, value
add_to_cartproduct addcurrency, value
remove_from_cartproduct removecurrency, value
view_cartcart viewcurrency, value
begin_checkoutorder startcurrency, value, coupon
add_shipping_infoorder shippingcurrency, value, tier
add_payment_infoorder paymentcurrency, value, type
purchaseorder completeid, currency, total, tax, shipping
refundorder refundid, currency, total
add_to_wishlistwishlist addcurrency, value

List / promotion​

GA4 (en)walkerOS (name)Fields
view_item_listlist viewid, name
select_itemproduct clicklist_id, list_name
view_promotionpromotion viewreads from items[0]
select_promotionpromotion clickreads from items[0]
select_contentcontent selecttype, id
GA4 (en)walkerOS (name)Fields
video_startvideo starttitle, duration, current, percent
video_progressvideo progresssame as video_start
video_completevideo completesame as video_start
form_startform startid, name, destination
form_submitform submitid, name, destination
searchsearch submitterm

Auth / lead / share​

GA4 (en)walkerOS (name)Fields
loginsession loginmethod
sign_upsession signupmethod
generate_leadlead generatecurrency, value
sharecontent sharemethod, type, id

Auto-fired noise (dropped by default)​

GA4 (en)Behavior
user_engagementignore: true
session_startignore: true
first_visitignore: true

These events are emitted automatically by gtag and rarely carry analytics intent. Override the rule if you need them.

Fallback​

GA4 (en)walkerOS (name)Fields
'*'ga4 trackdata.event_name = original en

Any GA4 event name not listed above falls through to '*' and produces a generic ga4 track walkerOS event. Override '*' to change the fallback rule globally.

Override a default field​

User config replaces the matching default rule per event name. Other events keep their defaults. To swap a field on purchase:

{
"transformers": {
"ga4": {
"package": "@walkeros/transformer-ga4",
"config": {
"settings": {
"mapping": {
"purchase": {
"name": "order complete",
"data": {
"map": {
"id": "params.ep.transaction_id",
"total": "params.epn.value",
"currency": "params.ep.currency",
"coupon": "params.ep.promo_code"
}
}
}
}
}
}
}
}
}

Because v1 uses replace semantics, the entire purchase rule is taken from user config: copy any default fields you want to keep. Additive per-field merge is on the roadmap.

Drop an event​

Set ignore: true on any key to prevent it from being emitted:

"settings": {
"mapping": {
"click": { "ignore": true }
}
}

This is how user_engagement, session_start, and first_visit are silenced by default.

Custom events​

Two patterns:

1. Override '*' to change the global fallback for unknown GA4 event names:

"settings": {
"mapping": {
"*": {
"name": "custom event",
"data": { "map": { "event_name": "name" } }
}
}
}

2. Add a specific key for an event you fire via gtag('event', '<your_name>', ...):

"settings": {
"mapping": {
"newsletter_subscribe": {
"name": "newsletter signup",
"data": { "map": { "source": "params.ep.source" } }
}
}
}

Tracking ID filtering​

By default only Measurement IDs starting with G- are accepted; Ads (AW-) and DC (DC-) hits are dropped. Widen via a string regex in settings.tidPattern:

"settings": {
"tidPattern": "^(G|AW|DC)-"
}

The string is compiled to a RegExp at init time.

Caveats​

  • Replace semantics, not merge. A user mapping rule fully replaces the matching default rule. There is no per-field merge inside data.map in v1.
  • GA4 v2 only. Assumes the v2 Measurement Protocol layout (ep., epn., up., upn., prN, gcs). v1 is out of scope.
  • G- tids only by default. Override tidPattern to capture Ads and DC traffic.
  • Basic gcs only. Maps G1XX to marketing/analytics booleans. Functional/preferences flags and the newer gcd parameter are not decoded.
  • Body must be raw text. The transformer parses POST bodies as URL-encoded form lines. Pre-parsed JSON bodies will not decode.
  • Ingest contract is required. Source wiring must populate ctx.ingest.url (required) and ctx.ingest.body (optional) for batched hits.

Roadmap​

  • Additive per-field merge so partial overrides extend the default rule instead of replacing it
  • Web ingest via interception sources for capturing gtag traffic from the browser
  • More vendor decoders (Segment, Snowplow, Adobe) following the same before-chain pattern
  • Richer consent decoding (gcd, functional/preferences flags)

Next steps​

💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)