> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# GA4

<!-- -->

[Server](#)[ ](https://github.com/elbwalker/walkerOS/tree/main/packages/transformers/ga4)

<!-- -->

[Source code](https://github.com/elbwalker/walkerOS/tree/main/packages/transformers/ga4)[ ](https://www.npmjs.com/package/@walkeros/transformer-ga4)

<!-- -->

[Package](https://www.npmjs.com/package/@walkeros/transformer-ga4)

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 traffic from an existing `gtag`/Google Tag setup: you point the tag at your own collector, and your `gtag('event', ...)` calls stay as they are. One HTTP request can carry many GA4 events; the transformer returns one walkerOS event per GA4 event in the hit.

The scope: server-side decoding in front of a server source, GA4 v2 only. See [Caveats](#caveats) and [Roadmap](#roadmap) for the boundaries.

## Installation[​](#installation "Direct link to Installation")

```
npm install @walkeros/transformer-ga4
```

## Send gtag hits to your collector[​](#send-gtag-hits-to-your-collector "Direct link to Send gtag hits to your collector")

gtag.js sends GA4 hits to `/g/collect` on the host it is configured with. To send them to your walkerOS collector instead of Google, set `server_container_url` on the GA4 config to your collector's origin:

```
gtag('config', 'G-XXXXXXXXXX', {

  server_container_url: 'https://collect.example.com',

});
```

Hits then arrive at `https://collect.example.com/g/collect`. If the URL carries a path (for example `https://example.com/metrics`), gtag.js appends `/g/collect` to it, and that full path is what the server source must listen on. Hits go to your collector instead of Google, so GA4 only receives what your flow forwards.

If gtag runs through the walkerOS [gtag destination](/preview/pr-720/docs/destinations/web/gtag/ga4.md), set the same value in its settings: `settings.ga4.server_container_url` (or `settings.ga4.transport_url`), which it passes to the `config` command.

The request URL, method, and body format described on this page are gtag.js behaviour, not a walkerOS contract, and can change with gtag.js releases. Confirm them in your browser's Network tab (filter for `collect`) before you rely on them.

## Wire it up[​](#wire-it-up "Direct link to Wire it up")

The transformer reads `ctx.ingest.url` (required) and `ctx.ingest.body` (optional) from the source it sits in front of. gtag.js can send several events in one POST body, so the source must keep a body that is not JSON as `ingest.body`. These server sources do, so pick by runtime:

* `@walkeros/server-source-express`, the source `walkeros run` and the `walkeros/flow` Docker image serve. A `text/plain` body that is valid JSON is parsed, and any other `text/plain` body reaches the transformer as the raw string.
* `@walkeros/server-source-fetch`, for a runtime that serves a `(Request) => Response` handler (Cloudflare Workers, Deno, Bun, or Node.js 18+ with a fetch adapter). See [Batched POST hits](#batched-post-hits) for the wiring.
* `@walkeros/server-source-gcp` (`sourceCloudFunction`) on Google Cloud Functions.
* `@walkeros/server-source-aws` (`sourceLambda`) on AWS Lambda. It decodes batched POSTs and GET hits, but answers a POST without a body with 400, so single-event hits sent that way are lost.

The example below uses the express source. It listens on `/collect` by default, so set `paths` to the path gtag.js sends to. Without it, GA4 hits get a 404:

* Bundled
* Integrated

```
{

  "version": 4,

  "flows": {

    "default": {

      "config": { "platform": "server" },

      "sources": {

        "http": {

          "package": "@walkeros/server-source-express",

          "config": {

            "settings": {

              "port": 8080,

              "paths": ["/g/collect"]

            },

            "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" }

      }

    }

  }

}
```

```
import { startFlow } from '@walkeros/collector';

import { sourceExpress } from '@walkeros/server-source-express';

import { transformerGa4 } from '@walkeros/transformer-ga4';

import { destinationDemo } from '@walkeros/destination-demo';



await startFlow({

  sources: {

    http: {

      code: sourceExpress,

      config: {

        settings: {

          port: 8080,

          paths: ['/g/collect'],

        },

        ingest: {

          map: {

            url: { key: 'url' },

            path: { key: 'path' },

            method: { key: 'method' },

            body: { key: 'body' },

          },

        },

      },

      before: 'ga4',

    },

  },

  transformers: {

    ga4: { code: transformerGa4 },

  },

  destinations: {

    log: { code: destinationDemo },

  },

});
```

### Ingest contract[​](#ingest-contract "Direct link to Ingest contract")

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

| Key    | Type     | Required | Notes                                                                                        |
| ------ | -------- | -------- | -------------------------------------------------------------------------------------------- |
| `url`  | `string` | yes      | Full request URL including the query string.                                                 |
| `path` | `string` | no       | Request path. Use it to gate the `before` chain when the source also receives other traffic. |
| `body` | `string` | no       | Raw POST body. Multi-event batches are `\n`-separated lines.                                 |

`config.ingest` must use the `map` operator with **direct field paths on the request scope** (no `req.` prefix), as shown above. A bare object like `{ "url": "req.url" }` is silently inert: without an operator no field is extracted, `ctx.ingest` stays empty, and the transformer drops the hit.

If `url` is missing or not a string the transformer drops the event silently.

### GA4 hits and walkerOS events on one source[​](#ga4-hits-and-walkeros-events-on-one-source "Direct link to GA4 hits and walkerOS events on one source")

With `before: "ga4"` every request on the source runs through the decoder, and a request without GA4 parameters is dropped. To receive walkerOS events on the same source, listen on both paths and gate the chain on `ingest.path`:

```
"http": {

  "package": "@walkeros/server-source-express",

  "config": {

    "settings": { "port": 8080, "paths": ["/collect", "/g/collect"] },

    "ingest": {

      "map": {

        "url": { "key": "url" },

        "path": { "key": "path" },

        "method": { "key": "method" },

        "body": { "key": "body" }

      }

    }

  },

  "before": {

    "match": { "key": "ingest.path", "operator": "eq", "value": "/g/collect" },

    "next": "ga4"

  }

}
```

### Batched POST hits[​](#batched-post-hits "Direct link to Batched POST hits")

gtag.js sends a single event as a POST with the parameters in the query string and no body, and several events as one POST whose body holds one event per line (check this in the Network tab, as above). The body arrives as `text/plain`, and `@walkeros/server-source-express` keeps a `text/plain` body that is not JSON as `ingest.body`, so the express example above decodes GET hits, body-less POST hits, and batched POST hits alike.

`@walkeros/server-source-fetch`, `sourceCloudFunction` (`@walkeros/server-source-gcp`), and `sourceLambda` (`@walkeros/server-source-aws`) also keep a body that is not JSON as `ingest.body`, so batched hits decode there with the same `ingest` and `before` settings (the Lambda source rejects a POST without a body, see above). The fetch source also takes `paths`, but has no `port` setting: it is a `(Request) => Response` handler you hand to your runtime. On Cloudflare Workers:

```
import { startFlow } from '@walkeros/collector';

import { sourceFetch } from '@walkeros/server-source-fetch';

import { transformerGa4 } from '@walkeros/transformer-ga4';

import { destinationDemo } from '@walkeros/destination-demo';



const { collector } = await startFlow({

  sources: {

    http: {

      code: sourceFetch,

      config: {

        settings: { paths: ['/g/collect'] },

        ingest: {

          map: {

            url: { key: 'url' },

            path: { key: 'path' },

            method: { key: 'method' },

            body: { key: 'body' },

          },

        },

      },

      before: 'ga4',

    },

  },

  transformers: {

    ga4: { code: transformerGa4 },

  },

  destinations: {

    log: { code: destinationDemo },

  },

});



export default { fetch: collector.sources.http.push };
```

`walkeros run` and the `walkeros/flow` Docker image only serve a source that exposes a Node HTTP handler, which the fetch source does not, so a bundled flow with the fetch source needs a runtime that calls its `push` with each `Request`.

## Configuration[​](#configuration "Direct link to Configuration")

This <!-- -->transformer<!-- --> uses the standard <!-- -->transformer<!-- --> config wrapper (consent, data, env, id, ...). For the shared fields see [transformer<!-- --> configuration](/docs/transformers#configuration). Package-specific fields live under `config.settings` and are listed below.

## Settings[​](#settings "Direct link to Settings")

| Property     | Type                     | Description                                                                                                                                                                                  | More |
| ------------ | ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---- |
| `mapping`    | `Record<string, object>` | Mapping rules keyed by GA4 event name (\`en\`), \`'\*'\` for unknown events. A rule replaces the matching default unless it sets \`extend\` or \`remove\`; \`ignore: true\` drops the event. |      |
| `tidPattern` | `string`                 | Regex string the tracking ID (\`tid\`) must match, compiled once at init. Default \`^G-\`, which drops Ads (\`AW-\`) and DC (\`DC-\`) hits.                                                  |      |
| `maxEvents`  | `integer`                | Maximum number of events (POST body lines) decoded from one request. A request above the cap is dropped whole. Default 100.                                                                  |      |

## Mapping[​](#mapping "Direct link to Mapping")

This package does not define custom rule-level settings. For the standard rule fields (consent, condition, data, batch, name, policy) see [mapping](/docs/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&_s=4&cid=cid-1&sid=1700000000&en=add_to_cart&ep.currency=EUR&epn.value=129.99"
}
```

Out

```
return {
  "id": "a6836917c43be032",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "4"
  },
  "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, each with its own id.

Event

```
{
  "url": "https://www.google-analytics.com/g/collect?v=2&tid=G-EXAMPLE&_p=p1&_s=12&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": "8ab72f078097141f",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "12"
  },
  "consent": {},
  "name": "product add",
  "entity": "product",
  "action": "add",
  "data": {
    "currency": "EUR",
    "value": 19.99
  }
};

return {
  "id": "8aba3307809950c2",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "12"
  },
  "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&_s=5&cid=cid-1&sid=1700000000&en=begin_checkout&ep.currency=EUR&epn.value=149.97&ep.coupon=WELCOME10"
}
```

Out

```
return {
  "id": "e9a2d80f15837449",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "5"
  },
  "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&_s=10&cid=cid-1&sid=1700000000&gcs=G100&en=page_view&dl=https%3A%2F%2Fx&dt=X"
}
```

Out

```
return {
  "id": "8edc3aea57048d5d",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "10"
  },
  "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&_s=9&cid=cid-1&sid=1700000000&en=newsletter_subscribe&ep.source=footer"
}
```

Out

```
return {
  "id": "09f644269b418d05",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "9"
  },
  "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&_s=8&cid=cid-1&sid=1700000000&en=login&ep.method=google"
}
```

Out

```
return {
  "id": "073e353121b1d68e",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "8"
  },
  "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&_s=1&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": "e696fb64aa2977fd",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "1"
  },
  "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&_s=2&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": "502e44f9d66b4f50",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "2"
  },
  "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&_s=6&cid=cid-1&sid=1700000000&en=scroll&epn.percent_scrolled=90"
}
```

Out

```
return {
  "id": "68d641a4df21861c",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "6"
  },
  "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&_s=7&cid=cid-1&sid=1700000000&en=search&ep.search_term=trail%20runner"
}
```

Out

```
return {
  "id": "f7293057f63a752b",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "7"
  },
  "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&_s=11&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&_s=3&cid=cid-1&sid=1700000000&en=view_item&ep.currency=EUR&epn.value=129.99"
}
```

Out

```
return {
  "id": "32d273af6128083f",
  "timestamp": 1700000000000,
  "timing": 0,
  "trigger": "ga4",
  "user": {
    "device": "cid-1",
    "session": "1700000000"
  },
  "globals": {},
  "source": {
    "type": "ga4",
    "pageLoadId": "p1",
    "hitSequence": "3"
  },
  "consent": {},
  "name": "product view",
  "entity": "product",
  "action": "view",
  "data": {
    "currency": "EUR",
    "value": 129.99
  }
}
```

## Default mappings[​](#default-mappings "Direct link to 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](/preview/pr-720/docs/getting-started/event-model.md).

### Page / scroll / click[​](#page--scroll--click "Direct link to Page / scroll / click")

| GA4 (`en`)      | walkerOS (`name`) | Fields                      |
| --------------- | ----------------- | --------------------------- |
| `page_view`     | `page view`       | `id`, `title`, `referrer`   |
| `scroll`        | `page scroll`     | `percent`                   |
| `click`         | `link click`      | `url`, `domain`, `outbound` |
| `file_download` | `file download`   | `name`, `extension`, `url`  |

### Ecommerce[​](#ecommerce "Direct link to Ecommerce")

| GA4 (`en`)          | walkerOS (`name`) | Fields                                                 |
| ------------------- | ----------------- | ------------------------------------------------------ |
| `view_item`         | `product view`    | `currency`, `value`                                    |
| `add_to_cart`       | `product add`     | `currency`, `value`                                    |
| `remove_from_cart`  | `product remove`  | `currency`, `value`                                    |
| `view_cart`         | `cart view`       | `currency`, `value`                                    |
| `begin_checkout`    | `order start`     | `currency`, `value`, `coupon`                          |
| `add_shipping_info` | `order shipping`  | `currency`, `value`, `tier`                            |
| `add_payment_info`  | `order payment`   | `currency`, `value`, `type`                            |
| `purchase`          | `order complete`  | `id`, `currency`, `total`, `tax`, `shipping`, `coupon` |
| `refund`            | `order refund`    | `id`, `currency`, `total`                              |
| `add_to_wishlist`   | `wishlist add`    | `currency`, `value`                                    |

### List / promotion[​](#list--promotion "Direct link to List / promotion")

| GA4 (`en`)         | walkerOS (`name`) | Fields                 |
| ------------------ | ----------------- | ---------------------- |
| `view_item_list`   | `list view`       | `id`, `name`           |
| `select_item`      | `product click`   | `list_id`, `list_name` |
| `view_promotion`   | `promotion view`  | reads from `items[0]`  |
| `select_promotion` | `promotion click` | reads from `items[0]`  |
| `select_content`   | `content select`  | `type`, `id`           |

### Video / form / search[​](#video--form--search "Direct link to Video / form / search")

| GA4 (`en`)       | walkerOS (`name`) | Fields                                    |
| ---------------- | ----------------- | ----------------------------------------- |
| `video_start`    | `video start`     | `title`, `duration`, `current`, `percent` |
| `video_progress` | `video progress`  | same as `video_start`                     |
| `video_complete` | `video complete`  | same as `video_start`                     |
| `form_start`     | `form start`      | `id`, `name`, `destination`               |
| `form_submit`    | `form submit`     | `id`, `name`, `destination`               |
| `search`         | `search submit`   | `term`                                    |

### Auth / lead / share[​](#auth--lead--share "Direct link to Auth / lead / share")

| GA4 (`en`)      | walkerOS (`name`) | Fields                 |
| --------------- | ----------------- | ---------------------- |
| `login`         | `session login`   | `method`               |
| `sign_up`       | `session signup`  | `method`               |
| `generate_lead` | `lead generate`   | `currency`, `value`    |
| `share`         | `content share`   | `method`, `type`, `id` |

### Auto-fired noise (dropped by default)[​](#auto-fired-noise-dropped-by-default "Direct link to Auto-fired noise (dropped by default)")

| GA4 (`en`)        | Behavior       |
| ----------------- | -------------- |
| `user_engagement` | `ignore: true` |
| `session_start`   | `ignore: true` |
| `first_visit`     | `ignore: true` |

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

### Fallback[​](#fallback "Direct link to Fallback")

| GA4 (`en`) | walkerOS (`name`) | Fields                            |
| ---------- | ----------------- | --------------------------------- |
| `'*'`      | `ga4 track`       | `data.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[​](#override-a-default-field "Direct link to Override a default field")

User config applies per event name; other events keep their defaults. A rule with `extend` or `remove` patches the matching default rule. A rule with neither replaces it.

### Patch a default rule[​](#patch-a-default-rule "Direct link to Patch a default rule")

`extend` is deep-merged onto the default rule, so you add or change individual fields and keep the rest. `remove` strips fields from the produced `data`. To add `affiliation` to `purchase` and drop `currency`:

```
{

  "transformers": {

    "ga4": {

      "package": "@walkeros/transformer-ga4",

      "config": {

        "settings": {

          "mapping": {

            "purchase": {

              "extend": {

                "data": { "map": { "affiliation": "params.ep.affiliation" } }

              },

              "remove": ["currency"]

            }

          }

        }

      }

    }

  }

}
```

`id`, `total`, `tax`, `shipping`, and `coupon` stay as the default defines them. A `null` value in `extend` clears an inherited field, for example `"extend": { "name": null }`.

### Replace a default rule[​](#replace-a-default-rule "Direct link to Replace a default rule")

Without `extend` or `remove`, the whole rule is taken from your config:

```
{

  "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"

                }

              }

            }

          }

        }

      }

    }

  }

}
```

The default `tax` and `shipping` fields are gone and `coupon` now reads `promo_code`: a replacing rule keeps only what it lists.

## Drop an event[​](#drop-an-event "Direct link to 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[​](#custom-events "Direct link to 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[​](#tracking-id-filtering "Direct link to 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.

## Batch size limit[​](#batch-size-limit "Direct link to Batch size limit")

Every decoded event continues through the pipeline as its own push, and a source's batch limit (such as the express `maxBatchSize`) only covers JSON batches, not a raw `text/plain` body. The transformer therefore caps the events in one request with `settings.maxEvents` (default `100`). It counts the non-empty lines of the POST body before decoding, and a request with more lines than the cap is dropped whole: no event from it is decoded, and one `warn` log line records the line count and the cap. The source still answers the client with success, so that log line is the only sign of the drop. Raise the cap only if your own server-to-server calls send larger batches:

```
"settings": {

  "maxEvents": 250

}
```

## Caveats[​](#caveats "Direct link to Caveats")

* **Replace unless `extend` or `remove` is set.** A user mapping rule without either keyword fully replaces the matching default rule.
* **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.
* **Batched POST hits need a raw body.** The source must keep a `text/plain` body that is not JSON as `ingest.body`. The express, fetch, Cloud Function, and Lambda sources do; the Lambda source rejects a POST without a body. See [Batched POST hits](#batched-post-hits).
* **At most 100 events per request by default.** A POST body with more lines than `settings.maxEvents` is dropped whole. See [Batch size limit](#batch-size-limit).
* **Ingest contract is required.** Source wiring must populate `ctx.ingest.url` (required) and `ctx.ingest.body` (optional) for batched hits.

## Roadmap[​](#roadmap "Direct link to Roadmap")

* **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[​](#next-steps "Direct link to Next steps")

* **[Create your own](/preview/pr-720/docs/transformers/create-your-own.md)** - Build custom transformers
* **[Server source: express](/preview/pr-720/docs/sources/server/express.md)** - Pair the decoder with the HTTP source
