Google Analytics 4 (GA4)
Google Analytics 4 (GA4) is configured within the unified gtag destination. walkerOS loads the gtag script and forwards mapped events to GA4.
Installation
npm install @walkeros/web-destination-gtagimport { startFlow } from '@walkeros/collector';
import { destinationGtag } from '@walkeros/web-destination-gtag';
await startFlow({
destinations: {
ga4: {
code: destinationGtag,
config: {
settings: {
ga4: {
measurementId: 'G-XXXXXXXXXX',
},
},
},
},
},
});Let a tag manager own the configuration
If a container (for example GTM) already configures your measurement ID, set
init: false. walkerOS then skips the gtag bootstrap entirely, no js, no
config, no script load, and only sends events, routed by send_to.
settings: {
ga4: {
measurementId: 'G-XXXXXXXX',
init: false,
},
}This matters for attribution. A config command affects user and session
identity for the rest of the page, so a second one arriving later than the
container's own can disturb the session GA4 already established. When the
container configures the tag, let it stay authoritative and put
server_container_url and related settings on the container's tag rather than
here.
Serve gtag.js first-party
By default gtag.js loads from googletagmanager.com. To serve it from your own
tagging server instead, set scriptSrc to the
tag serving path
configured in your server container. The path maps to the measurement ID inside
the container, so the ID is not part of the URL.
settings: {
ga4: {
measurementId: 'G-XXXXXXXXXX',
scriptSrc: 'https://example.com/metrics/tag_serving_path/',
server_container_url: 'https://example.com/metrics',
},
}server_container_url routes measurement data and must not include the tag
serving path. Setting it alone does not change where the script loads from,
because serving gtag.js is a separate capability your container may not have. If
scriptSrc fails to load, walkerOS falls back to googletagmanager.com so a
misconfigured container degrades to working measurement rather than none.
A page view needs no mapping
The gtag destination snake-cases an entity action event name into a GA4 event
name automatically, so page view arrives in GA4 as page_view with no mapping
at all. An unmapped product add would arrive as product_add carrying only
send_to, which is why the ecommerce event below needs an explicit mapping.
Add the ecommerce mapping
Nest the mapping under the gtag destination to turn a product add event into
the GA4 add_to_cart event. The product price becomes the GA4 value, the
currency falls back to EUR, and the product is shaped into a GA4 items array
via a this loop.
mapping: {
product: {
add: {
name: 'add_to_cart',
data: {
map: {
value: 'data.price',
currency: { value: 'EUR', key: 'data.currency' },
items: {
loop: [
'this',
{
map: {
item_id: 'data.id',
item_name: 'data.name',
quantity: { value: 1, key: 'data.quantity' },
},
},
],
},
},
},
},
},
}Pushing a product add event:
elb('product add', { id: 'ers', name: 'Everyday Ruck Snack', price: 420 });produces the GA4 add_to_cart event:
gtag('event', 'add_to_cart', {
value: 420,
currency: 'EUR',
items: [{ item_id: 'ers', item_name: 'Everyday Ruck Snack', quantity: 1 }],
send_to: 'G-XXXXXXXXXX',
});Parameter names
A parameter name carrying a character GA4 does not accept, a hyphen most
commonly, is discarded on ingestion without an error. The destination therefore
rewrites those characters to underscores before sending. Tagging creative-type
under include: ['data'] produces the parameter data_creative-type, which is
sent as data_creative_type rather than going missing. Case is left alone, since
GA4 treats parameter names case-sensitively.
Event names are never rewritten this way. snakeCase lowercases the event name
and replaces its spaces with underscores; set it to false and the name is sent
exactly as configured.
Two parameters are dropped rather than renamed:
| Parameter | Why |
|---|---|
source_platform | Produced by include from event.source.platform. GA4 reads it as a manual campaign carrying no source or medium, which reports traffic as Unassigned |
user_id | Produced by include from event.user.id. GA4 takes identity through its own configuration and ignores the name as an event parameter |
Required ecommerce parameters such as currency, value and transaction_id
are unaffected. To keep the platform value, map it to a name of your own:
data: { map: { platform: 'source.platform' } }Verify in GA4
Copy your Measurement ID from GA4 > Admin > Data Streams and open your web
stream. Watch Reports > Realtime to confirm the page_view event, then open
Admin > DebugView to inspect the add_to_cart event and its parameters as they
arrive.
Configuration
This destination uses the standard destination config wrapper (consent, data, env, id, ...). For the shared fields see destination configuration. Package-specific fields live under config.settings and are listed below.
Settings
| Property | Type | Description | More |
|---|---|---|---|
como | boolean | object | Consent mode configuration: false (disabled), true (use defaults), or custom mapping | |
como_advanced | boolean | number | Advanced consent mode (non-EU): emit denied default at page load with wait_for_update (true=500ms, or a number of ms) | |
ga4 | ga4 | GA4-specific configuration settings | |
measurementId | string | GA4 Measurement ID from Google Analytics | |
debug | boolean | Enable debug mode for GA4 | |
pageview | boolean | Enable automatic pageview tracking | |
server_container_url | string | Server-side GTM container URL | |
snakeCase | boolean | Lowercase the event name and replace its spaces with underscores (like true). Applies to the event name only, never to parameter names | |
transport_url | string | Custom transport URL for GA4 | |
data | any | Custom data mapping configuration | |
ads | ads | Google Ads specific configuration settings | |
conversionId | string | Google Ads Conversion ID (required) | |
currency | string | Currency code (ISO 4217, e.g., USD, EUR) | |
data | any | Custom data mapping (WalkerOS.Mapping.Value | Values) | |
enhancedConversions | enhancedConversions | Enhanced conversions: maps event fields to Google user_data for improved measurement accuracy | |
email | any | Mapping value for user email | |
phone_number | any | Mapping value for user phone number | |
address | address | Address mapping for enhanced conversions | |
first_name | any | Mapping value for first name | |
last_name | any | Mapping value for last name | |
street | any | Mapping value for street address | |
city | any | Mapping value for city | |
region | any | Mapping value for region/state | |
postal_code | any | Mapping value for postal/zip code | |
country | any | Mapping value for country | |
gtm | gtm | Google Tag Manager specific configuration settings | |
containerId | string | GTM Container ID (required) | |
dataLayer | string | Custom dataLayer variable name (default: dataLayer) | |
domain | string | Custom GTM domain for script loading | |
data | any | Custom data mapping (WalkerOS.Mapping.Value | Values) |
Mapping
Per-event rules under config.mapping. For the standard rule fields (consent, condition, data, batch, name, policy) see mapping.
| Property | Type | Description | More |
|---|---|---|---|
ga4 | ga4 | GA4-specific event mapping | |
ads | ads | Google Ads-specific event mapping | |
label | string | Conversion label for this specific event | |
gtm | gtm | GTM-specific event mapping |
Examples
Add to cart
A product add event is mapped to the GA4 add_to_cart event with item details and value.
{
"name": "product add",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"intent",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [],
"consent": {
"functional": true
},
"id": "982aaf8f6f907448",
"trigger": "click",
"entity": "product",
"action": "add",
"timestamp": 1700000101,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}{
"name": "add_to_cart",
"include": [
"data"
],
"data": {
"map": {
"currency": {
"value": "EUR",
"key": "data.currency"
},
"value": "data.price",
"items": {
"loop": [
"this",
{
"map": {
"item_id": "data.id",
"item_variant": "data.color",
"quantity": {
"value": 1,
"key": "data.quantity"
}
}
}
]
}
}
}
}gtag("event", "add_to_cart", {
"currency": "EUR",
"value": 420,
"items": [
{
"item_id": "ers",
"item_variant": "black",
"quantity": 1
}
],
"data_id": "ers",
"data_name": "Everyday Ruck Snack",
"data_color": "black",
"data_size": "l",
"data_price": 420,
"send_to": "G-XXXXXX-1"
})Enhanced conversions
enhancedConversions maps event fields into a gtag set user_data call sent immediately before the conversion event.
{
"name": "order complete",
"data": {
"id": "0rd3r1d",
"currency": "EUR",
"shipping": 5.22,
"taxes": 73.76,
"total": 555,
"customerEmail": "buyer@shop.com",
"customerPhone": "+1234567890"
},
"context": {
"shopping": [
"complete",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "product",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "product",
"data": {
"id": "cc",
"name": "Cool Cap",
"size": "one size",
"price": 42
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "gift",
"data": {
"name": "Surprise"
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
}
],
"consent": {
"functional": true
},
"id": "6b430a240c7b5595",
"trigger": "load",
"entity": "order",
"action": "complete",
"timestamp": 1700000109,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}{
"name": "PURCHASE_EC2",
"settings": {
"ads": {
"label": "PURCHASE_EC2"
}
},
"data": {
"map": {
"value": "data.total"
}
}
}gtag("set", "user_data", {
"email": "buyer@shop.com",
"phone_number": "+1234567890"
});
gtag("event", "conversion", {
"send_to": "AW-123456789/PURCHASE_EC2",
"currency": "EUR",
"value": 555
})Google Ads init
Configures a Google Ads account via its conversionId. gtag already exists here, so no `js` is sent.
{
"settings": {
"ads": {
"conversionId": "AW-123456789",
"currency": "EUR"
}
}
}gtag("config", "AW-123456789")Consent Mode v2
A walker consent command with marketing and functional granted updates gtag Consent Mode v2 parameters.
{
"marketing": true,
"functional": true
}gtag("consent", "default", {
"ad_storage": "denied",
"ad_user_data": "denied",
"ad_personalization": "denied",
"analytics_storage": "denied"
});
gtag("consent", "update", {
"ad_storage": "granted",
"ad_user_data": "granted",
"ad_personalization": "granted",
"analytics_storage": "granted"
})GA4 init
Configures a GA4 property via its measurementId. gtag already exists here, so no `js` is sent: that would be a second, false initialisation signal.
{
"settings": {
"ga4": {
"measurementId": "G-XXXXXX-1"
}
}
}gtag("config", "G-XXXXXX-1", {})GA4 include all
Include flattens every event section into prefixed GA4 params, exposing data, context, user, source, and event fields.
{
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "walkerOS documentation",
"referrer": "https://www.walkeros.io/",
"search": "?foo=bar",
"hash": "#hash",
"id": "/docs/"
},
"context": {
"dev": [
"test",
1
]
},
"globals": {
"pagegroup": "docs"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "child",
"data": {
"is": "subordinated"
}
}
],
"consent": {
"functional": true
},
"id": "ev-1700000106",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000106,
"timing": 3.14,
"source": {
"count": 1,
"trace": "1700000106abcdef1700000106abcdef",
"type": "collector",
"schema": "4"
}
}{
"include": [
"data",
"context",
"globals",
"user",
"source",
"event"
]
}gtag("event", "page_view", {
"data_domain": "www.example.com",
"data_title": "walkerOS documentation",
"data_referrer": "https://www.walkeros.io/",
"data_search": "?foo=bar",
"data_hash": "#hash",
"data_id": "/docs/",
"context_dev": "test",
"globals_pagegroup": "docs",
"user_device": "c00k13",
"user_session": "s3ss10n",
"source_type": "collector",
"source_schema": "4",
"source_count": 1,
"source_trace": "1700000106abcdef1700000106abcdef",
"source_release_default": "4.4.0",
"event_entity": "page",
"event_action": "view",
"event_trigger": "load",
"event_id": "ev-1700000106",
"event_name": "page view",
"event_timestamp": 1700000106,
"event_timing": 3.14,
"send_to": "G-XXXXXX-1"
})Ads conversion
An order complete event is sent as a Google Ads conversion with a configured label and transaction value.
{
"name": "order complete",
"data": {
"id": "0rd3r1d",
"currency": "EUR",
"shipping": 5.22,
"taxes": 73.76,
"total": 555
},
"context": {
"shopping": [
"complete",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "product",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "product",
"data": {
"id": "cc",
"name": "Cool Cap",
"size": "one size",
"price": 42
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "gift",
"data": {
"name": "Surprise"
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
}
],
"consent": {
"functional": true
},
"id": "13cf56bb87d0ee8f",
"trigger": "load",
"entity": "order",
"action": "complete",
"timestamp": 1700000103,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}{
"name": "PURCHASE_CONV",
"settings": {
"ads": {
"label": "PURCHASE_CONV"
}
},
"data": {
"map": {
"value": "data.total"
}
}
}gtag("event", "conversion", {
"send_to": "AW-123456789/PURCHASE_CONV",
"currency": "EUR",
"value": 555
})GTM dataLayer push
A page view event is pushed to window.dataLayer for GTM with the mapped event name and parameters.
{
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "walkerOS documentation",
"referrer": "https://www.walkeros.io/",
"search": "?foo=bar",
"hash": "#hash",
"id": "/docs/"
},
"context": {
"dev": [
"test",
1
]
},
"globals": {
"pagegroup": "docs"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "child",
"data": {
"is": "subordinated"
}
}
],
"consent": {
"functional": true
},
"id": "67dbe2e094f5e5b9",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000104,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}{
"name": "page_view",
"settings": {
"gtm": {}
},
"data": {
"map": {
"page_title": "data.title",
"page_location": "data.domain"
}
}
}dataLayer.push({
"event": "page_view",
"page_title": "walkerOS documentation",
"page_location": "www.example.com"
})GTM init
With script loading enabled the destination initializes the dataLayer, pushes the gtm.js start event, and loads the GTM container.
{
"loadScript": true,
"settings": {
"gtm": {
"containerId": "GTM-XXXXXXX"
}
}
}dataLayer.push({
"gtm.start": 1700000000000,
"event": "gtm.js"
})Multi-tool push
A single order event fans out to GA4, Google Ads, and GTM from one mapping rule with per-tool settings.
{
"name": "order complete",
"data": {
"id": "0rd3r1d",
"currency": "EUR",
"shipping": 5.22,
"taxes": 73.76,
"total": 555
},
"context": {
"shopping": [
"complete",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "product",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "product",
"data": {
"id": "cc",
"name": "Cool Cap",
"size": "one size",
"price": 42
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "gift",
"data": {
"name": "Surprise"
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
}
],
"consent": {
"functional": true
},
"id": "2e5bfeed9cb33187",
"trigger": "load",
"entity": "order",
"action": "complete",
"timestamp": 1700000107,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}{
"name": "purchase",
"include": [
"data"
],
"settings": {
"ga4": {},
"ads": {
"label": "PURCHASE_CONV"
},
"gtm": {}
},
"data": {
"map": {
"value": "data.total",
"currency": {
"key": "data.currency",
"value": "EUR"
}
}
}
}gtag("event", "purchase", {
"value": 555,
"currency": "EUR",
"data_id": "0rd3r1d",
"data_currency": "EUR",
"data_shipping": 5.22,
"data_taxes": 73.76,
"data_total": 555,
"send_to": "G-XXXXXX-1"
});
gtag("event", "conversion", {
"send_to": "AW-123456789/PURCHASE_CONV",
"currency": "EUR",
"value": 555,
"data_id": "0rd3r1d",
"data_currency": "EUR",
"data_shipping": 5.22,
"data_taxes": 73.76,
"data_total": 555
});
dataLayer.push({
"event": "purchase",
"value": 555,
"currency": "EUR",
"data_id": "0rd3r1d",
"data_currency": "EUR",
"data_shipping": 5.22,
"data_taxes": 73.76,
"data_total": 555
})Page view
A page view event is forwarded as a GA4 page_view event with no additional mapping.
{
"name": "page view",
"data": {
"domain": "www.example.com",
"title": "walkerOS documentation",
"referrer": "https://www.walkeros.io/",
"search": "?foo=bar",
"hash": "#hash",
"id": "/docs/"
},
"context": {
"dev": [
"test",
1
]
},
"globals": {
"pagegroup": "docs"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "child",
"data": {
"is": "subordinated"
}
}
],
"consent": {
"functional": true
},
"id": "431afb8d4c52676f",
"trigger": "load",
"entity": "page",
"action": "view",
"timestamp": 1700000102,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}gtag("event", "page_view", {
"send_to": "G-XXXXXX-1"
})Purchase
An order complete event is mapped to the GA4 purchase event with transaction details and nested product items.
{
"name": "order complete",
"data": {
"id": "0rd3r1d",
"currency": "EUR",
"shipping": 5.22,
"taxes": 73.76,
"total": 555
},
"context": {
"shopping": [
"complete",
0
]
},
"globals": {
"pagegroup": "shop"
},
"custom": {
"completely": "random"
},
"user": {
"id": "us3r",
"device": "c00k13",
"session": "s3ss10n"
},
"nested": [
{
"entity": "product",
"data": {
"id": "ers",
"name": "Everyday Ruck Snack",
"color": "black",
"size": "l",
"price": 420
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "product",
"data": {
"id": "cc",
"name": "Cool Cap",
"size": "one size",
"price": 42
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
},
{
"entity": "gift",
"data": {
"name": "Surprise"
},
"context": {
"shopping": [
"complete",
0
]
},
"nested": []
}
],
"consent": {
"functional": true
},
"id": "ab03081ed9b63dc2",
"trigger": "load",
"entity": "order",
"action": "complete",
"timestamp": 1700000100,
"timing": 3.14,
"source": {
"count": 1,
"trace": "0a1b2c3d4e5f60718293a4b5c6d7e8f9",
"type": "collector",
"schema": "4"
}
}{
"name": "purchase",
"include": [
"data",
"context"
],
"data": {
"map": {
"transaction_id": "data.id",
"value": "data.total",
"tax": "data.taxes",
"shipping": "data.shipping",
"currency": {
"key": "data.currency",
"value": "EUR"
},
"items": {
"loop": [
"nested",
{
"condition": {
"$code": "e=>S(e)&&\"product\"===e.entity"
},
"map": {
"item_id": "data.id",
"item_name": "data.name",
"quantity": {
"key": "data.quantity",
"value": 1
}
}
}
]
}
}
}
}gtag("event", "purchase", {
"transaction_id": "0rd3r1d",
"value": 555,
"tax": 73.76,
"shipping": 5.22,
"currency": "EUR",
"items": [
{
"item_id": "ers",
"item_name": "Everyday Ruck Snack",
"quantity": 1
},
{
"item_id": "cc",
"item_name": "Cool Cap",
"quantity": 1
}
],
"data_id": "0rd3r1d",
"data_currency": "EUR",
"data_shipping": 5.22,
"data_taxes": 73.76,
"data_total": 555,
"context_shopping": "complete",
"send_to": "G-XXXXXX-1"
})