# Google Tag Manager installation

There are two ways to install Telltale with Google Tag Manager:

- **The Telltale template (recommended).** A tag template made for your Site, with consent control and identity fields built in. The sections from [The Telltale template](#the-telltale-template) to [Identity](#identity) cover it; [Loading behaviour](#loading-behaviour) and the sections after it give the details.
- **A Custom HTML tag.** Paste a few lines into a Custom HTML tag, if you prefer not to import a template. See [Custom HTML tag](#custom-html-tag).

Use one of them per site, never both.

GTM steps checked against the official Google Tag Manager documentation on 2026-11-03: [Custom HTML tags](https://support.google.com/tagmanager/answer/6107167), [Consent mode and tag consent settings](https://support.google.com/tagmanager/answer/10718549), [Using Tag Manager with a Content Security Policy](https://developers.google.com/tag-platform/security/guides/csp), [Preview and debug containers](https://support.google.com/tagmanager/answer/6107056).

## The Telltale template

Download the `.tpl` from **Connect a site → Download GTM template**. The generated file contains the selected site's public key and an exact script-injection permission for this collector's `/sdk.js`. It contains no server key. GTM requires HTTPS even for imported script permissions. Configure `PUBLIC_ORIGIN` (or the public edge) to an exact trusted HTTPS collector origin. An HTTP collector returns a clear 400 error instead of an invalid template.

Before downloading, list any **custom consent types** your CMP sets (comma-separated, for example `telltale_measurement`). The template always grants read access to Google's standard types (`ad_storage`, `analytics_storage`, `ad_user_data`, `ad_personalization`, `functionality_storage`, `personalization_storage`, `security_storage`); each custom type you list gets its own read row in the `access_consent` permission. For a template you already imported, add the custom type under the template's **Permissions → Accesses consent state** instead. The template never writes consent state.

## Choose a collection mode

The tag has a **Let my CMP control collection** checkbox, **off by default** (changed 2026-10-28): like the hosted script tag, the tag starts recording on page load. Turn it on if a consent banner must decide first; EU and UK sites usually need one. A refusal already saved on the browser is honoured in both modes.

**Existing containers.** A container saved with the box checked keeps it checked and stays held until the CMP decides. A tag imported from an earlier template without a saved value for the box now starts on load; open the tag and check the box if your site relies on a consent banner.

- **Off (default, ordinary start).** For sites without a CMP. The SDK starts as soon as the tag fires; consent fields are ignored and no consent API is read. A refusal the SDK has already saved in that browser is still honored. Do not leave it off to work around a CMP.
- **On (CMP control).** Collection starts held. Only an actual CMP decision event can grant, and only when **CMP decision is granted** is true *and* every type in **Consent types that must all be granted** is granted in Google consent state (empty means `analytics_storage`). A continuing listener on every selected type withdraws as soon as any of them turns denied, even before another tag fires. Delayed SDK loading rereads consent before it delivers a grant.

Use one mode per site and page: a held and an ordinary tag for the same site conflict and the tag fails instead of silently overriding.

## Set up CMP control

1. Import the file in a GTM **Web** container: Templates → New → menu → Import. Save it and create a tag using that template.
2. In the CMP's **Consent Initialization** tag, set denied defaults for every selected consent type. Google treats an unset type as granted, so a missing default is not safe. This template does not set defaults or replace the CMP.
3. Enable GTM's built-in **Event** variable and create two Custom JavaScript variables that return real booleans (not the string `"true"`):

```js
// "Telltale consent decision event"
function () { return {{Event}} === 'telltale_consent_decision'; }
// "Telltale identity ready event"
function () { return {{Event}} === 'telltale_identity_ready'; }
```

4. Map **Apply current CMP decision** to the consent-decision variable, **CMP decision is granted** to your CMP's aggregate result for the purposes this deployment requires, and **Identity is current for this event** to the identity-ready variable.
5. Fire the tag once on every page after Consent Initialization, even while denied, so the withdrawal listeners are installed, and again on the `telltale_consent_decision` and `telltale_identity_ready` custom events. Do not use an Additional Consent Check or an "always fire" override that would block or bypass this bridge.
6. After the CMP has updated Google consent state, push the decision event for every saved decision on each page load and for every change, including a refusal that stays a refusal:

```js
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({ event: 'telltale_consent_decision' });
```

7. Validate with GTM Preview/Tag Assistant, then publish through your normal GTM process. Test fresh load, saved refusal, grant, withdrawal, regrant, delayed script loading, logout and account switching.

## Identity

Map **User ID** and **Account ID** to variables holding the current user's and account's stable internal IDs (prefer opaque IDs). If your container has no such variables, push both values with one event after authentication is ready, and on every login, logout and account switch:

```js
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'telltale_identity_ready',
  telltale_user_id: currentUser ? currentUser.id : null,
  telltale_account_id: currentAccount ? currentAccount.id : null
});
```

Create Data Layer Variables for `telltale_user_id` and `telltale_account_id`, map them to the tag's fields, and fire the tag on `telltale_identity_ready` with **Identity is current for this event** true (step 3). Push null values after logout. A changed user or account resets the local Telltale session before the replacement is associated.

Identity is event-scoped: it is delivered only on the identity event while collection is permitted, discarded while held, and never replayed after a withdrawal. After a regrant, fire a fresh identity event from your auth owner. These IDs are browser-supplied context, not verification; protected actions still need backend authentication and POST /api/v1/verify. No session token is ever written to the data layer or a tag variable.

## Custom HTML tag

A Custom HTML tag runs a few lines of your own JavaScript. Settings › Channels in the Telltale dashboard shows the plain tag with your real key filled in:

```html nocopy
<script src="https://app.trytelltale.dev/sdk/v1.js" data-telltale-key="YOUR_SITE_PUBLISHABLE_KEY"></script>
```

Do not paste that line into GTM as it is: GTM does not reliably keep `data-` attributes on elements in Custom HTML. The code below adds the same tag with JavaScript instead, so the attribute stays. It is written in older JavaScript (`var`, `function`) so any container accepts it.

### Sites without a consent banner

1. In GTM, go to Tags › New.
2. Choose **Tag Configuration** › **Custom HTML**, and paste:

```html
<script>
(function () {
  var SDK = 'https://app.trytelltale.dev/sdk/v1.js';
  if (window.Telltale || document.querySelector('script[src="' + SDK + '"]')) return; // already added
  var script = document.createElement('script');
  script.src = SDK;
  script.setAttribute('data-telltale-key', 'YOUR_SITE_PUBLISHABLE_KEY');
  document.head.appendChild(script);
})();
</script>
```

3. Choose **Triggering** › **All Pages**. (**Initialization - All Pages** also works and fires a little earlier. Do not use **Consent Initialization - All Pages**; that trigger is for setting consent defaults.)
4. Name the tag (for example "Telltale") and choose **Save**.
5. Check it in **Preview** (see [Check it worked](#check-it-worked)), then choose **Submit** › **Publish**.

### Wait for consent

If your site shows a consent banner, let GTM's consent settings hold the tag until measurement is allowed. The tag then adds the SDK held and calls `optIn()`, so a saved refusal from an earlier visit is replaced only by a new grant. A held tag sends nothing until `optIn()`.

1. Use this code in the Custom HTML tag instead:

```html
<script>
(function () {
  var SDK = 'https://app.trytelltale.dev/sdk/v1.js';
  function grant() {
    try { window.Telltale.getClient().optIn(); } catch (e) {}
  }
  if (window.Telltale) return grant(); // already loaded: a new grant
  if (document.querySelector('script[src="' + SDK + '"]')) return; // still loading: its onload grants
  var script = document.createElement('script');
  script.src = SDK;
  script.setAttribute('data-telltale-key', 'YOUR_SITE_PUBLISHABLE_KEY');
  script.setAttribute('data-telltale-wait-for-consent', '');
  script.onload = grant;
  document.head.appendChild(script);
})();
</script>
```

2. Open **Advanced Settings** › **Consent Settings**. Under **Additional Consent Checks**, choose **Require additional consent for tag to fire**, then **+ Add required consent** and enter `analytics_storage` (or the consent type your banner uses for this kind of measurement).
3. Under **Triggering**, add **All Pages** and also a **Custom Event** trigger for the event your consent banner pushes after the visitor makes or changes a choice. GTM checks consent at the moment a trigger fires, so without the second trigger the tag would wait until the next page.
4. Add a second Custom HTML tag, "Telltale withdraw", for when the visitor withdraws. Set its consent to **No additional consent required**, trigger it on the same consent-change event with a condition that matches a refusal (your banner's own data layer value), and paste:

```html
<script>
(function () {
  try { if (window.Telltale) window.Telltale.getClient().optOut(); } catch (e) {}
})();
</script>
```

5. Make sure your banner sets denied defaults on **Consent Initialization - All Pages**, as for the template.
6. Save, check in **Preview**, then **Submit** › **Publish**.

For a consent manager that already has a recipe ([OneTrust](/docs/consent-onetrust), [Cookiebot](/docs/consent-cookiebot), [Sourcepoint](/docs/consent-sourcepoint)), the template's **Let my CMP control collection** is the more thorough option: it listens for withdrawal itself. See also the [Consent bridge guide](/docs/consent).

### Identify the signed-in user

Send opaque, stable internal IDs for the user and the account. Never send e-mail addresses, names or phone numbers. The recommended way is from your server after sign-in ([Quickstart step 2](/docs/quickstart#2-attach-identity-from-your-own-authentication), [Server API › Associate identity](/docs/server#associate-identity)); the data layer gives a browser label on top.

1. When your auth knows who is signed in, and on every login, logout and account switch, push the IDs to the data layer:

```javascript
window.dataLayer = window.dataLayer || [];
window.dataLayer.push({
  event: 'telltale_identity_ready',
  telltale_user_id: currentUser ? currentUser.id : null,
  telltale_account_id: currentAccount ? currentAccount.id : null
});
```

2. In GTM, create two **Data Layer Variable** variables, "DLV - telltale_user_id" and "DLV - telltale_account_id", for those two keys.
3. Add a Custom HTML tag, "Telltale identify", triggered by a **Custom Event** trigger for `telltale_identity_ready`, with the same consent settings as the main tag, and paste:

```html
<script>
(function () {
  var userId = {{DLV - telltale_user_id}} || null;
  var accountId = {{DLV - telltale_account_id}} || null;
  function apply() {
    try {
      var client = window.Telltale.getClient();
      var done = userId || accountId
        ? client.identify({ userId: userId ? String(userId) : null, accountId: accountId ? String(accountId) : null })
        : client.reset(); // pushed with nulls: the user logged out
      done['catch'](function () {}); // rejects while held for consent or when the Telltale server is unreachable
    } catch (e) {}
  }
  if (window.Telltale) return apply();
  var tag = document.querySelector('script[src$="/sdk/v1.js"]');
  if (tag) tag.addEventListener('load', apply);
})();
</script>
```

Push null values only on logout, not on every page for visitors who are not signed in: each `reset()` starts a fresh session.

## Content Security Policy

If your site sends a Content Security Policy, the Telltale server must be allowed for scripts and connections, whichever way you install:

```http
Content-Security-Policy: script-src 'nonce-RANDOM_PER_RESPONSE' 'strict-dynamic'; connect-src 'self' https://app.trytelltale.dev
```

Use Google's nonce-aware container snippet with the same nonce: Tag Manager then passes the nonce to the scripts it adds, and `'strict-dynamic'` trusts the SDK script they add in turn. Your policy also needs the sources Google lists for Tag Manager itself. Never add `'unsafe-inline'` or `'unsafe-eval'` for Telltale.

GTM's Custom JavaScript variables only work with `'unsafe-eval'`, so under a strict policy the two Custom JavaScript variables in [Set up CMP control](#set-up-cmp-control) step 3 do not evaluate. The Custom HTML path above uses only Data Layer Variables. The [Content Security Policy guide](/docs/csp) has the full options.

## Check it worked

1. In GTM, choose **Preview**, enter your site's address and connect. In Tag Assistant, open the first page view and check that your Telltale tag is under **Tags Fired**. With a consent banner, it should fire only after you accept.
2. Publish the container: **Submit** › **Publish**. Preview only affects your own browser.
3. In the Telltale dashboard, open Settings › Channels and leave the **Is it working?** check open. Choose **Open my site to test it** and browse a page or two.
4. Watch the check move through **Script loaded on** your address, **First session received** and, if you set up identity, **Signed-in user linked**. When the first session arrives, it links to it.

In your browser's DevTools › Network, `sdk/v1.js` should answer 200, and so should `api/sdk/init`. If it stays at the first step, see [It's not working](/docs/troubleshooting).

## Common mistakes

- **Not publishing.** Saved tags do nothing until the container version is published.
- **Pasting the plain `<script ... data-telltale-key>` line into Custom HTML.** The key attribute can be lost. Use the code above, which sets it with JavaScript.
- **The template and a Custom HTML tag together, or GTM plus a tag in the page.** Use one installation per site.
- **A consent check with only the All Pages trigger.** The tag then waits until the next page after the visitor accepts. Add the consent-change trigger.
- **`www` and the bare domain.** `https://www.example.com` and `https://example.com` are different addresses. Add each one under Settings › Channels › Website addresses.
- **Ad blockers.** Some extensions block Tag Manager or third-party scripts. Test in a window without extensions.

## Have an agent install this

Give a coding or browser agent this prompt, filled in with your container and CMP details:

```text
Install the Telltale Google Tag Manager template for this site. Use the site-specific template downloaded from the Telltale dashboard (Connect a site > Download GTM template) and the official GTM documentation. Inspect the GTM Web container first: find the CMP's Consent Initialization tag, the consent types it sets, and existing variables that expose the current signed-in user's stable ID and the current account, organization or workspace ID. Check "Let my CMP control collection" (it is off by default) when the site has a CMP. Select every consent type the CMP requires for this vendor; if one is a custom type, add a read permission row for it. Make sure denied defaults are set for each selected type on Consent Initialization. Create the {{Event}}-based Custom JavaScript variables for telltale_consent_decision and telltale_identity_ready, and add the documented dataLayer pushes only where the site does not already emit an equivalent event. Map the identity variables; if none exist, add the telltale_identity_ready push and Data Layer Variables. Never put a server key, session token or credential in GTM. Verify in Tag Assistant: held start with no collector requests, grant, withdrawal, regrant, logout and account switch. Stop before accepting any terms, submitting to the Community Template Gallery or publishing a production container, and report what you verified.
```

## Loading behaviour

Repeated firings share one loader scope and use a cached script injection. The latest permission state wins if it changes during download. Duplicate script-load callbacks do not replay identity. An incompatible existing configuration for the same site fails instead of silently overriding it. Multiple sites keep separate managed owners and identity cancellation. Use one installation owner per site and page; don't combine this tag with another auth/consent synchronizer for the same SDK.

Remove the tag and publish to stop future installation. Withdrawal stops owned runtime work, but it cannot recall already accepted data. Historical data deletion is a separate operation.

## Permissions and fail-closed checks

The template declares four permissions: `inject_script` for exactly `<collector>/sdk.js`, `access_globals` to execute only `Telltale.installManaged`, `access_consent` to read (never write) the standard Google types plus any custom types listed at download, and `access_template_storage`. Before it touches storage, consent or the page, the sandbox calls `queryPermission` for each of them, including a read check for every selected consent type, and validates the public key (`ao_pk_` plus `A–Z a–z 0–9 _ -`, at most 128 characters) and, on identity events, the 128-character identity limit. Any failure calls `gtmOnFailure` and does nothing else, so permission drift (or a custom type without its permission row) fails loudly instead of half-installing. The managed installer returns `true` when it takes ownership of the callbacks; if the SDK global is missing, the tag fails instead of hanging.
## Template tests

Every generated `.tpl` carries a `___TESTS___` block (default start on load, held start, grant with identity, script-load failure, withdrawal during load, duplicate loads, each permission query, denied permission, missing installer, ordinary start without a CMP, every selected consent type listened to and required, delayed-load consent reread, a custom type without its permission row, over-long identity). Run them with **Run Tests** in the template editor, then check the tag in GTM Preview before you publish.

Attribution: the terms header, test-block structure, permission pre-checks, key alphabet check, field conventions and metadata shape are adapted from an Apache-2.0 licensed GTM template; its licence notice ships with Telltale. The consent bridge is Telltale's own.
