# Kibble installation contract: section 19 of 29 (guide version 3)

> This is **one section only**, not the whole contract. Complete document: https://www.usekibble.dev/AI/guide.md

## 19. Google Tag Manager {#gtm}

> **Status: direct integration.** The real SDK is used as written; exercised in the repository demos and browser tests.

### The standard tag (recommended)

One **Custom HTML** tag that carries only the public key, the same for every client. Everything else is set by a Kibble staff member on the property (Manage → Data tracking → Setup) and fetched by the script by key from `<collector origin>/sdk/v1/settings/<key>.json`: the consent source, the registered hosts and how links between them are carried, the cookie domain, whether the dataLayer and gtag entries are read, and what the site's own event names mean. Check that the deployed SDK lists `config.settings-by-key` in `/sdk/v1/features.json` before relying on it ([Versions and compatibility](https://www.usekibble.dev/AI#versions)).

Standard Custom HTML tag (the key is the only property value):

```html
<script>
(function (w, d) {
  if (w.__kibble || w.__kibbleLoading || w.__kibbleGtmLoading) return;
  w.__kibbleLoading = true;
  w.kibble = w.kibble || function () { (w.kibble.q = w.kibble.q || []).push(arguments); };
  var s = d.createElement('script');
  s.async = true;
  s.src = 'https://use.example-kibble-host/sdk/v1/kibble.js';
  s.setAttribute('data-key', 'pk_YOUR_PROPERTY_KEY');
  s.onerror = function () { w.__kibbleLoading = false; s.remove(); console.warn('[kibble] kibble.js did not load (blocked or offline)'); };
  d.head.appendChild(s);
})(window, document);
</script>
```

1. **Trigger:** Initialization – All Pages (or Page View – All Pages), limited to the property's hostnames with **Page Hostname equals**. Not a page-path trigger: journeys need every page. Limit by path only on a host the client does not want fully measured.
2. **Firing:** Advanced settings → Tag firing options → Once per page. **Consent settings: No additional consent required.** The script enforces the property's consent source itself; requiring `analytics_storage` in GTM stops the tag on sites without Consent Mode defaults (for example a booking engine whose consent platform is TrustArc).
3. **Same tag on the booking engine** when it runs the same container. Do not add `data-*` attributes to "configure" it: ask the Kibble staff member to change the setting instead.
4. **First-party gateway:** load `kibble.js` from the gateway and add `s.setAttribute('data-endpoint', 'https://www.example.com/measure')` so the settings and every request go through it. The gateway must allow `GET sdk/v1/settings/<key>.json` and `POST v1/event-names` ([gateways](https://www.usekibble.dev/AI#gateways)).
5. **One install.** If the site already installs the SDK in its source, a plugin or another tag, do not also add this tag; a second copy is ignored with a console warning.

### How settings by key behave

- **Only key-only installations fetch them.** An installation that sets `data-consent` (or `window.kibbleConfig.consent`) configures itself and never requests settings, so it does not change. Every snippet and recipe Kibble generated before 1.14.0 sets it; a hand-written installation with only `data-key` does not, and from 1.14.0 it reads the settings once a staff member saves them (until then it runs as before, with manual consent). On a key-only installation any other attribute you add (`data-datalayer`, `data-cookie-domain`, `data-connected-hosts`, `data-tenant`, `data-routes`, `data-storage`, `data-datalayer-gtag`) wins over the fetched value.
- **Nothing starts before they arrive.** Commands wait in the queue; `_kbh` is removed from the address first. The request carries no cookie and no referrer.
- **A failed fetch collects nothing extra.** If the settings cannot be fetched within four seconds, or the answer is not understood, the script runs as an installation without settings: consent is manual (nothing is collected unless the page calls `kibble('consent', 'granted')`), the dataLayer is not read, no cookie domain and no linking.
- **Saved settings only.** A property added to Kibble from 1.14.0 starts with saved defaults (Google Consent Mode, dataLayer read, gtag entries not read, unknown names listed). A property added earlier serves no settings until a staff member saves them: until then a key-only installation runs as one without settings. Ask the human whether the settings are saved before testing.
- **Five minutes.** The settings are cached for up to five minutes; a change in Kibble reaches live sites within that time. Kibble records every change (who, when, before and after).
- **On a provider's host** that is registered as an external connection, the script uses that connection's tenant and routes and property-scoped storage, and never writes a cookie there.
- **Hosts that share the cookie domain** are linked by the first-party cookie; links to other registered hosts carry the handoff ([gateways](https://www.usekibble.dev/AI#gateways)).

### Event names Kibble does not know

- When the dataLayer is read and learning is on, the script reports the **name only** of other dataLayer events, after consent, at most 20 names per page load, to `POST /v1/event-names` (never parameters and never a browser, visit or stream id). Names must be at most 60 characters of letters, digits, spaces and `_ . : -`; names with `@` or a run of six or more digits, and `gtm.*` tag-manager events, are dropped in the browser and again by the collector.
- Kibble staff decide what a name means on Data tracking → Events: `ignore`, `add_to_cart`, `checkout_step` (with a step), `availability` (optionally also a step), `purchase`, `booking`, `lead` (enquiry) or `candidate` (recorded, not counted). A decision for a standard name (for example `begin_checkout`) replaces the standard mapping on key-only installations.
- Each kind reads only its own fields (item, quantity, value, currency, reference, stay start and end, nights, adults, children, guests, results offered), from an event parameter path such as `ecommerce.transaction_id`, or for stay fields from a page-address parameter (`url:arrive`; the value is validated in the browser and the address itself is never sent). Paths that name personal data or free text (email, name, phone, address, search terms, `user_data`) are refused by Kibble and ignored by the script. References are hashed in the browser as everywhere else.
- An `availability` decision can also count **the same search once** (`repeat: criteria`), for results lists that re-send their event when the visitor sorts or the list re-renders: a push whose subject, subject type, arrival, nights and guests equal the previous availability that decision sent on the page is not counted again. Changed criteria count, and so does a return to earlier criteria after another search; whether results were offered is not part of the criteria. Without it every push counts (a double push of the same signal within 2 seconds counts once).
- Meaning stays a human decision: a purchase on a hotel is reviewed like any other ([Outcomes](https://www.usekibble.dev/AI#outcomes)); nothing here adds a counting rule. **Do not push personal data into the dataLayer** for Kibble: it never needs it.

### What Tag Manager costs (say it in the report)

- Visitors whose blockers stop `googletagmanager.com` or third-party analytics are not measured on a GTM-only site; a first-party gateway keeps them.
- GTM fires later than an inline script: very short visits and early timings are slightly thinner.
- Settings changed in Kibble take effect after the cache time (five minutes).
- A page whose Content Security Policy blocks the script host is not fixed by GTM.
- Anyone with container access can pause or duplicate the tag; the duplicate guard stays.

### Tag with page settings

Installations from before 1.14.0, and any site whose developer wires consent on the page, use this form instead: consent and hosts are set on the script tag, so it never reads settings from Kibble.

1. Create a **Custom HTML** tag with the code below, triggered once per page on Initialization, after the CMP sets defaults on Consent Initialization. Scope the trigger to the host the tag is for: add the condition **Page Hostname equals** the configured host (the website host, or the connection host for a provider's page). A container that is shared by several hosts must not fire this tag on the others.
2. Choose **No additional consent required** for this loader: the SDK starts denied and follows analytics_storage itself. The CMP must publish default and update commands to dataLayer. Blocking the loader at page load does not arrange a later grant trigger; if policy forbids loading the SDK before consent, a separately verified CMP grant trigger is required.
3. **One install.** If the site already installs the snippet in its source (or a plugin, theme or another tag does), do not also install it through GTM. The tag below returns when an SDK or a Kibble SDK script (`script[data-key][src*="/sdk/v1/kibble"]`) is already on the page; another vendor's `data-key` script does not stop it, and a second SDK copy is ignored with a console warning, but two installations can still disagree on options and one of them is wasted. Do not map `page_view` yourself: the SDK records page loads and single-page navigation. A GTM history-change trigger that calls Kibble would count page views twice.
4. **Shared provider hosts.** When the tag runs on a host shared with other businesses (for example `guest.rezstream.com`), the hostname trigger is not enough: the tag must check the page's own tenant marker itself and return when it is not this business, and it must set `data-tenant`. See the rezStream recipe in [Booking engines and booking widgets](https://www.usekibble.dev/AI#booking) for a worked example.
5. Keep the asynchronous queue stub in the tag (it is the first line of the code below). A consent or outcome call that runs before the SDK has loaded is held by it. Do not replace it with a check for `window.kibble`.
6. Consent: `google-consent-mode` follows `analytics_storage`. Do not map advertising storage to Kibble.
7. **Dedupe outcomes.** A `purchase` or `booking_confirmed` dataLayer push that the page fires on every render of the confirmation step is deduplicated by reference inside one SDK instance (one page load); a once-per-page flag in the tag or a once-only guard from the page itself (for example a render counter the platform exposes) is still the safer rule. Do not send the same outcome from both the dataLayer and an explicit `kibble('outcome')` call.
8. **Approved fields only.** Push or send only the fields in the allowlist table below. Never put names, emails, phone numbers, addresses, free text, cart ids, order contents or form values in a dataLayer push, a tag variable or a tag call.

Custom HTML tag with page settings:

```html
<script>
(function () {
  window.kibble=window.kibble||function(){(window.kibble.q=window.kibble.q||[]).push(arguments)};
  if (window.__kibble || document.querySelector('script[data-key][src*="/sdk/v1/kibble"]')) return;   // Kibble already installed once: do not add a second SDK
  var s = document.createElement('script');
  s.async = true;
  s.src = 'https://use.example-kibble-host/sdk/v1/kibble.js';
  s.setAttribute('data-key', 'pk_YOUR_PROPERTY_KEY');
  s.setAttribute('data-endpoint', 'https://use.example-kibble-host');
  s.setAttribute('data-consent', 'google-consent-mode');
  s.setAttribute('data-datalayer', 'on');
  // Only if the site pushes gtag('event', …) commands: s.setAttribute('data-datalayer-gtag', 'on');
  // On a provider's page (an external connection), also: data-tenant, data-storage="property" and data-connected-hosts="<website host>"
  document.head.appendChild(s);
})();
</script>
```

### Check it locally

- **GTM Preview:** connect the preview to the page; the Kibble tag fires exactly once per page load, on the configured host only, and the loader runs once, while collection stays stopped until analytics_storage is granted; test a late grant and withdrawal without reloading.
- **Network tab:** one `kibble.js` request; before consent no `/v1/collect`; after consent a `/v1/collect` answered 200. On a connection host the request body has `tn` (the tenant).
- **Duplicates:** one `page_view` per page load and per single-page route change, not two. Two page views for one load mean a second installation (theme, plugin or tag) or a manual mapping.
- **Handoff:** after following a link from another host, `_kbh` is no longer in the address bar and the first page view has `nav: handoff`.
- **Outcomes:** a test confirmation produces one `outcome` (or one `outcome_candidate`), not two.

### Custom Template

A Custom Template (`integrations/gtm/kibble-sdk.tpl`, prepared for the Community Template Gallery but **not listed there yet**) is the same key-only installation as the standard tag, as a sandboxed tag: the Load action installs the same queue stub (`createArgumentsQueue('kibble', 'kibble.q')`), sets `window.kibbleConfig` to the public key and the endpoint only (template scripts cannot set `data-*` attributes; [The snippet and its options](https://www.usekibble.dev/AI#snippet)), and loads `kibble.js` from Kibble's production collector, the only script address its permission allows. Consent, hosts, the cookie domain and the dataLayer come from the settings by key, exactly as for the standard tag. Use the same trigger and consent settings as the standard tag. Caveats:

- It sets nothing but the key and the endpoint: setting consent or other options on the page would switch off the settings Kibble serves by key. A site that needs page settings uses the Custom HTML tag with page settings.
- **Gateway:** the optional gateway address sends the settings request and collection through a first-party gateway, but the script itself still loads from the collector. Gallery templates cannot have editable permissions, so to load `kibble.js` from the gateway too (blockers, or a Content Security Policy that blocks the collector) use the standard Custom HTML tag with `data-endpoint`.
- Grant consent and Withdraw consent actions call `kibble('consent', …)`; before the script has loaded the call waits in the queue. Fire them only from a trigger that reflects the visitor's choice: the property's consent source in Kibble applies as well.
- One install: it loads nothing when Kibble is already running or another Kibble tag (this template or the standard tag) is loading it.
- No tenant, routes or storage fields: for a provider's host use the Custom HTML tag.
- The template has not been imported into a live container.

### The dataLayer allowlist

**gtag command entries (opt-in).** Sites that call `gtag('event', 'purchase', { transaction_id: 'T1', value: 10, currency: 'USD', items: [...] })` push an Arguments object or an array, not an object. Add `data-datalayer-gtag="on"` (`1` and `true` also work; requires `data-datalayer`) to map `event` commands whose name is in the allowlist below, with the same fields. It is off by default, so existing installations do not change. `config`, `consent`, `set`, `js` and `get` commands are ignored (consent is read only through `data-consent="google-consent-mode"`), and only the fields listed below are read from the parameters; nothing else is copied. If the site already sends the same purchase both as a gtag command and a dataLayer object, the transaction reference keeps the outcome counted once, but product and checkout events would be counted twice: send each event one way.

With `data-datalayer="on"` (`1` and `true` also work) the SDK wraps `dataLayer.push` once (other listeners and return values are preserved) and maps only this allowlist. Everything else is ignored. Entries pushed before consent is granted are not replayed after a later grant, so a confirmation pushed too early is lost.

| dataLayer event | Becomes | Fields read |
| --- | --- | --- |
| `view_item` (GA4) | `product_view` | `items[0].item_id`, `item_category`, `price` |
| `add_to_cart` (GA4) | `add_to_cart` | `items[0].item_id`, `quantity`, `price`, `value` |
| `begin_checkout`, `add_shipping_info`, `add_payment_info` (GA4) | `checkout_step` cart / shipping / payment | `value` |
| `purchase` (GA4) | outcome: purchase only with `transaction_id` | `transaction_id`, `value`, `currency`, item quantities |
| `generate_lead`, `sign_up` (GA4) | outcome candidate only | nothing but the signal name |
| `kibble_outcome` | outcome. `purchase` and `booking` need `reference`; a `lead` without one is recorded as a lead | `outcome_type`, `reference`, `value`, `currency`, `form` |
| `availability_search` (alias `kibble_availability`) | availability (the booking engine's results only, not the website's booking form) | `hotel_id` or `destination`, `check_in`, `nights`, `guests`, `available` |
| `tee_time_search` (golf courses) | availability for a course (a tee-time search) | `course_id`, `play_date`, `tee_time` (course-local `HH:MM`), `players` (1 to 8), `holes` (9 or 18), `available` |
| `tee_time_selected` (golf courses) | `checkout_step` `tee_time` | nothing |
| `booking_step` | `checkout_step` | `step` |
| `booking_confirmed` | outcome: booking only with `booking_ref` | `booking_ref`, `value`, `currency` |

- Only the first item of an `items` array is read.
- `hotel_id`, `item_id` and other ids follow the [id rules](https://www.usekibble.dev/AI#markup): a run of six or more digits needs a prefix with a colon (`synxis:1234567`), or the id is dropped. Use the same prefixed form in the [Site catalog](https://www.usekibble.dev/AI#site-catalog).
- A `purchase` or `booking_confirmed` without its reference produces a candidate, not an outcome ([Outcomes](https://www.usekibble.dev/AI#outcomes)).
- Product categories with spaces are dropped by the collector ([Semantic markup](https://www.usekibble.dev/AI#markup)).

END OF SECTION gtm
