> 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 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](#caveats) and [Roadmap](#roadmap) for the boundaries.

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

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

## 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. The recommended pairing is `@walkeros/server-source-express`:

* Bundled
* Integrated

```
{

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

      }

    }

  }

}
```

```
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: {

        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` | yes      | Request path, drives the `/g/collect` `before` match.        |
| `body` | `string` | no       | Raw 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[​](#configuration "Direct link to Configuration")

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

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

This package has no package-specific settings.

## 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](https://www.walkeros.io/docs/mapping.md).

## 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[​](#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](https://www.walkeros.io/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` |
| `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 **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](#roadmap).

## 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.

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

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