# Codingrow Google Tag Manager Data Layer — documentation

Magento 2 module that installs the Google Tag Manager container and feeds it a complete GA4
ecommerce data layer, built on the server from the real cart and the real order.

Two levels of the same module:

- **Container and core events** — free, no key to paste: the container plus `view_item`,
  `view_cart`, `begin_checkout` and `purchase`.
- **Shopping events** — paid licence, one domain: ten more events, custom dimensions, Google
  Consent Mode v2 and the recovery of purchases that never reach Analytics.

Compatible with Magento 2.4.x (Open Source and Adobe Commerce), PHP 8.1 to 8.5, Hyvä and Luma.

---

## Install

```
composer require codingrow/module-gtmdl
bin/magento module:enable Codingrow_Gtmdl
bin/magento setup:upgrade
```

In production mode also `setup:di:compile`, `setup:static-content:deploy` and `cache:flush`.

The Composer credentials arrive by email when you request the free module or buy the shopping
events.

---

## Configure

**Stores → Configuration → Codingrow Extensions → Google Tag Manager Data Layer.**

Every setting can be different per **store view**: change the *Scope* selector at the top left.

### License

Shows the state of both levels and holds the two key fields. The free level needs no key; the
shopping events have their own.

### Container

| Field | What it does |
|---|---|
| Enable | With this off the store renders no container and no data layer at all. |
| Container ID | Your Google Tag Manager container, in the form `GTM-XXXXXXX`. Leave it empty to push the data layer without loading any container — which is what you want when the container is already installed by something else. |
| Load the container only after cookie consent | Uses the Cookie Restriction Mode of Magento. The data layer is pushed either way, so nothing is lost once consent arrives. Keep it off when consent is handled by a dedicated extension, and see *Google consent* below. |

### Google consent

Google Consent Mode v2. The default state has to be declared **before the container starts**, so
only whoever loads the container can do it.

| Field | What it does |
|---|---|
| Consent mode | *Off*; *Declare the default state only*; *Declare the default state and update it*. |
| Wait for the update (milliseconds) | How long Google waits for an update before deciding. 500 is Google's suggestion; it matters with banners drawn after the page. |
| Redact advertising data without consent | While advertising consent is denied, Google sends no identifiers in its advertising calls. |
| Pass the click id through the URL | Without cookies, the click identifier travels in the address from page to page. |

Choose **Declare the default state only** when consent is already handled by a dedicated
extension: the module declares everything denied, and your extension sends the update by calling

```js
window.codingrowGtmdl.consent({
    ad_storage: 'granted',
    ad_user_data: 'granted',
    ad_personalization: 'granted',
    analytics_storage: 'granted'
});
```

Choose **Declare the default state and update it** to let the module grant consent from Magento's
own cookie notice. That option needs the **Cookie Restriction Mode of Magento** switched on
(*Stores → Configuration → General → Web → Default Cookie Settings*): with it off, no customer can
ever accept and consent would stay denied forever. A line under the fields says which of the two is
really happening and turns red in that case.

With consent mode active the container loads straight away with everything denied, so *Load the
container only after cookie consent* no longer applies: they are two alternative ways of respecting
the same thing.

**How to check it works**: in the browser's network panel, the requests to
`google-analytics.com/g/collect` carry a parameter `gcs`. It reads `G100` while consent is denied
and `G111` once it is granted. If it changes when you accept the cookies, Consent Mode is working.

### Events

One switch per event. The four core events are free; the other ten need the shopping events key.

| Event | When |
|---|---|
| `view_item` | product page |
| `view_cart` | cart page |
| `begin_checkout` | checkout page |
| `purchase` | success page, built from the real order |
| `view_item_list` | category and search results |
| `select_item` | a click on a product in a list |
| `add_to_cart` | from the real cart line |
| `remove_from_cart` | from the real cart line |
| `search` | the term searched and how many results it gave |
| `add_to_wishlist` | the product added |
| `add_shipping_info` | the shipping method saved |
| `add_payment_info` | the payment method saved |
| `login` | customer sign-in |
| `sign_up` | customer registration |

Two more settings sit in this group:

- **Count orders created in the admin** — orders typed in by hand have no visit behind them, and
  counting them distorts the cost per conversion.
- **Only these order statuses** — restricts `purchase` to the statuses that really count as a sale.

`add_shipping_info` and `add_payment_info` do not depend on the checkout: they are raised from
inside Magento, when the shipping and the payment method are saved. They therefore work on the
standard checkout, on Hyvä, on Luma and on checkouts that answer on their own route. `view_item`,
`view_cart`, `begin_checkout` and `purchase` are page events: the module ships a small layout file
per route, and support can add one for a checkout that lives somewhere else.

### Container for Google Tag Manager

A data layer nobody reads looks like a broken module: until the container has a tag that reads it,
nothing reaches Analytics.

| Field | What it does |
|---|---|
| GA4 Measurement ID | In the form `G-XXXXXXXXXX`, from Analytics under Admin → Data streams. |
| Google Ads conversion ID | Digits only, the part after `AW-`. Leave empty to skip the Google Ads conversion. |
| Google Ads conversion label | The label of the purchase conversion action. |
| Download the container | Downloads the file to import. |

The file contains the GA4 configuration tag, the tag that forwards the ecommerce events, the
conversion linker, the variables they need and — when the two Google Ads fields are filled — the
purchase conversion carrying the real revenue and the order number.

In Google Tag Manager: **Admin → Import Container**, choose the file, pick **Merge** and *Rename
conflicting tags*, look at the preview, then publish.

### Recover the purchases that get lost

Roughly one purchase in five never reaches Analytics: an ad blocker, a script that failed, a
customer who closes the page too early. With this on, the store sends the purchase itself when the
browser did not.

| Field | What it does |
|---|---|
| Enable | Needs the GA4 Measurement ID above and the API secret below. |
| Measurement Protocol API secret | In Analytics: Admin → Data streams → your stream → Measurement Protocol API secrets → Create. Stored encrypted. |
| Wait before sending (minutes) | The margin given to the customer browser. Fifteen is enough even on a slow connection. |
| Also recover purchases with no Analytics identifier | For customers who block Analytics from the first page: there is no identifier to read, so the purchase can only be sent as a new, direct visit. You get the revenue back and lose the attribution for those orders. |

Under the fields, a panel counts the last thirty days: orders recorded, the ones the browser
confirmed, the ones the store recovered, the ones still waiting. A button asks Google to check the
content of a test purchase without recording it.

Three things worth knowing:

1. **Orders placed before you switched it on are not recovered**: recording starts with the switch.
2. **A wrong API secret produces no error.** Google accepts the call and discards the event without
   saying anything, so the module never claims a purchase "arrived": it records that it sent it.
   The confirmation is in Analytics, under Realtime, where a recovered purchase must appear.
3. **Customers who refused cookies are never recovered**, not even with the switch above: sending
   their purchase from the server would work around their refusal.

### What goes into the items

| Field | What it does |
|---|---|
| Prices include tax | Must match the basis your Google reports use. |
| Purchase value includes shipping | Changes only the `value` of the purchase. |
| Product attribute used as item_id | Use the same identifier as your product feed, or Google will not match the items. |
| Product attribute used as item_brand | Any product attribute. |
| Add the product categories | Adds `item_category` to each item. |
| Maximum items in view_item_list | Keeps a long category page from sending a huge event. |
| Also send the classic remarketing keys | `ecomm_pagetype` and `product_id`, for containers that still read them. |
| Ignored IP addresses | Comma separated. No event is sent from those addresses. |
| Custom dimensions | Pairs of *product attribute → GA4 parameter name*. The parameter travels inside every item of every event. Declare it in GA4 as an **item-scoped** custom dimension, otherwise it arrives but does not show up in the reports. |
| Extra name for the unique event id | For containers whose triggers read that id under another name. The same value travels under both names, so on switchover day you touch nothing in the container. |

---

## Troubleshooting

**"I set the container ID but nothing shows up in Google Tag Manager."** Check that *Enable* is Yes
in the scope you are looking at, that the ID has the form `GTM-XXXXXXX` (anything else is ignored),
and in production mode that you ran `setup:di:compile`, `setup:static-content:deploy` and
`cache:flush`. In Google Tag Manager's **Preview** you see the events arrive; if they arrive and
the reports stay empty, the problem is in the tags inside the container.

**"`add_to_cart` never fires."** It is one of the ten shopping events: the key goes in *Shopping
Events License Key*. Without it the store keeps sending the four core events.

**"The checkout events all arrive together on the thank-you page."** Normal on a single-page
checkout: the event is recorded the moment the customer chooses, and delivered to the data layer on
the next page load, which there is the thank-you page. In GA4 they stay in the same visit and in
the right order, before the purchase.

**"The numbers do not match Google Analytics or Google Ads."** Check *Prices include tax* against
the basis your reports use, *Purchase value includes shipping*, and that *Product attribute used as
item_id* is the identifier your product feed uses.

**"`purchase` arrived twice."** Not from the module: reloading the thank-you page does not repeat
the event. Look for a second tag sending the same purchase — for instance an old Google Ads
conversion still configured in *Stores → Configuration → Sales → Google API*.

**"We sell more than Analytics reports."** See *Recover the purchases that get lost* above.

---

## What the module does not do

- It does not create or change the tags inside Google Tag Manager: that stays the container's job,
  and the downloadable file is a starting point you own.
- It sends nothing to Google on its own, except the purchases you ask it to recover, which go to
  your own GA4 property with your own credentials.
- It collects no customer data for Codingrow.
