@owlmeans/web-gtm 0.1.18-rc.26 → 0.1.18-rc.27

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,7 +6,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
6
6
  your project's skill store (`.agents/skills/`):
7
7
 
8
8
  ```sh
9
- npx @owlmeans/agent-skills@^0.1.18-rc.33
9
+ npx @owlmeans/agent-skills@^0.1.18-rc.34
10
10
  ```
11
11
 
12
12
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/web-gtm",
4
- "version": "0.1.18-rc.26",
5
- "generatedAt": "2026-09-22T17:37:10.898Z",
4
+ "version": "0.1.18-rc.27",
5
+ "generatedAt": "2026-09-22T20:45:24.042Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -8,7 +8,7 @@ user-invocable: false
8
8
  # @owlmeans/web-gtm
9
9
 
10
10
  **Layer:** Web
11
- **Install:** `"@owlmeans/web-gtm": "^0.1.18-rc.26"` in `dependencies`
11
+ **Install:** `"@owlmeans/web-gtm": "^0.1.18-rc.27"` in `dependencies`
12
12
 
13
13
  The tag half of the consent set. It emits **strings and data**, not components, and it holds no
14
14
  state — the decision lives in `@owlmeans/consent`, which this package reads through
@@ -25,51 +25,20 @@ for the window that matters*, and nothing in the page reports it.
25
25
  So every head snippet here emits the consent bootstrap first and the loader after it, as ONE
26
26
  inline script. Everything else in this package exists to serve that.
27
27
 
28
- ## Loading mode: gated by default
29
-
30
- `googleTagHeadScript`, `gtmHeadScript` and `loadGtm` all take an optional `mode:
31
- 'basic' | 'advanced'`, defaulting to `GOOGLE_TAG_DEFAULT_MODE` — currently `'basic'`, one named
32
- constant so flipping the platform default later is a one-line change:
33
-
34
- | `mode` | What runs, and when |
35
- |---|---|
36
- | `'basic'` (default) | The bootstrap and the ads-redaction flags still run unconditionally — Consent Mode's own denied-by-default signals are declared either way. The LOADER itself — `gtm.js`, or `gtag/js` + `js` + `config` — is wrapped in `@owlmeans/consent`'s `consentGateScript`: it runs immediately if the visitor's stored record already `trackingGranted`, and otherwise waits for a later `CONSENT_EVENT` (from `applyConsent`, i.e. an in-page consent decision) that does. Nothing — not even Google's own IP receipt — reaches the tag before a signal-bearing category (`analytics` or `marketing` by default; a `required` or signal-less category never counts) is granted. |
37
- | `'advanced'` | The original, only behavior before this mode existed: the loader runs immediately, Consent Mode signals denied by default, so the tag itself starts receiving traffic cookielessly from first paint. This is Google's own recommended default for its conversion modeling, and remains available for a site that has decided that tradeoff is acceptable. |
38
-
39
- Read `'basic'`'s default as the EU/DE worst-case reading of ePrivacy Art. 5(3): no third party
40
- receives a connection before consent.
41
-
42
- `gtmNoscriptFrame` returns `''` in `'basic'` mode rather than an iframe — a browser with JavaScript
43
- disabled cannot have granted anything, so an unauthenticated `<noscript>` frame would defeat the
44
- whole point of gating. It keeps emitting the iframe in `'advanced'` mode, unchanged.
45
-
46
- `loadGtm` gates the same way at the DOM level: it still calls `consentStore.init(opts)` first, but
47
- only appends the container's `<script>` element once `trackingGranted(consentStore.get().record,
48
- opts.categories)` is true — immediately if already granted, or via a one-shot `consentStore.subscribe`
49
- that unsubscribes itself on the first update where it becomes true. The anti-double-load guard (the
50
- element's own id) is unchanged.
51
-
52
- None of this is new state: it is one more consumer of `@owlmeans/consent`'s existing surface —
53
- `trackingGranted(record, categories)` (whether a stored/applied record grants a signal-bearing,
54
- non-required category) and `consentGateScript(loaderExpr, opts)` (the inline script implementing the
55
- "run now or wait for `CONSENT_EVENT`" branch). Both live in `@owlmeans/consent`, not here, because a
56
- target project's own custom loader can reuse the same gate without depending on this package.
57
-
58
28
  ## Key Exports
59
29
 
60
30
  | Export | Description |
61
31
  |--------|-------------|
62
- | `googleTagHeadScript(opts)` | The inline `<head>` script for any Google id: consent bootstrap, ads redaction, then the loader the prefix calls for — gated behind `consentGateScript` in `opts.mode`'s default, `'basic'`. **The default for new code** |
32
+ | `googleTagHeadScript(opts)` | The inline `<head>` script for any Google id: consent bootstrap, ads redaction, then the loader the prefix calls for. **The default for new code** |
63
33
  | `isGoogleTagId(id)` / `googleTagKind(id)` | Whether an id is loadable, and whether it takes `gtm` (container) or `gtag` (gtag.js); `null` for anything else |
64
- | `GoogleTagOptions` / `GoogleTagKind` | `ConsentOptions` plus `id`, optional `dataLayerName` and `mode`; `'gtm' \| 'gtag'` |
65
- | `GoogleTagMode` / `GOOGLE_TAG_DEFAULT_MODE` | `'basic' \| 'advanced'`; the platform default, `'basic'` — see **Loading mode** below |
34
+ | `GoogleTagOptions` / `GoogleTagKind` | `ConsentOptions` plus `id` and optional `dataLayerName`; `'gtm' \| 'gtag'` |
66
35
  | `GOOGLE_TAG_CSP_SOURCES` | The hosts GA4, Tag Manager and Google Ads need in `script-src`, `connect-src` and `img-src` (7, frozen) |
67
36
  | `GOOGLE_TAG_FRAME_SOURCES` | The hosts they frame, for `frame-src`: `https://www.googletagmanager.com`, `https://td.doubleclick.net` |
68
37
  | `googleTagServices(id)` | The `ConsentService[]` a cookie policy discloses for that tag — pass to `CookiePolicy`'s `services` |
69
- | `gtmHeadScript(opts)` | The container-only snippet: consent defaults, stored decision, then `gtm.js` — gated the same way as `googleTagHeadScript` |
70
- | `gtmNoscriptFrame(opts)` | The hidden `<noscript>` iframe, for the top of `<body>` — `''` in `'basic'` mode |
71
- | `loadGtm(opts)` | Script-side container load, for a host that cannot emit into its own head — gated the same way |
72
- | `GtmOptions` | `ConsentOptions` plus `id` (e.g. `GTM-XXXXXXX`), optional `dataLayerName` and `mode` |
38
+ | `gtmHeadScript(opts)` | The container-only snippet: consent defaults, stored decision, then `gtm.js` |
39
+ | `gtmNoscriptFrame(opts)` | The hidden `<noscript>` iframe, for the top of `<body>` |
40
+ | `loadGtm(opts)` | Script-side container load, for a host that cannot emit into its own head |
41
+ | `GtmOptions` | `ConsentOptions` plus `id` (e.g. `GTM-XXXXXXX`) and optional `dataLayerName` |
73
42
 
74
43
  ## Ids
75
44
 
@@ -95,14 +64,11 @@ Lower case is rejected rather than folded; normalise in the form if you want to
95
64
  owns it, so app code can send events), `js`, `config`, and the gtag.js `<script async>`
96
65
  element created from the inline script — one inline script rather than Google's two elements,
97
66
  so a build stamps one thing. The element carries id `owl-gtag-<id>`, so running the snippet
98
- twice configures the tag once instead of double-counting page views. In `mode`'s default,
99
- `'basic'`, this whole step 3 is what `consentGateScript` withholds — steps 1 and 2 always run;
100
- pass `mode: 'advanced'` to run step 3 unconditionally too, as before this mode existed.
67
+ twice configures the tag once instead of double-counting page views.
101
68
 
102
- An **invalid id yields the consent bootstrap alone, regardless of `mode`** — a mistyped tag must
103
- never cost a page its consent defaults. So a host may stamp `googleTagHeadScript({ id: configured
104
- })` unconditionally, or stamp `consentBootstrapScript()` when no tag is configured; both are
105
- correct.
69
+ An **invalid id yields the consent bootstrap alone** — a mistyped tag must never cost a page its
70
+ consent defaults. So a host may stamp `googleTagHeadScript({ id: configured })` unconditionally,
71
+ or stamp `consentBootstrapScript()` when no tag is configured; both are correct.
106
72
 
107
73
  **The output is safe to place inline in HTML as-is.** JSON encoding keeps configured values inside
108
74
  their string literals, but the HTML parser ends a script element at the first `</script` wherever
@@ -182,13 +148,7 @@ cookie page does so when a Google tag is set. The strings are English data, not
182
148
  - **`loadGtm` is the fallback, not the default.** It is for a single-page app whose HTML is not
183
149
  yours to edit. It still pushes the defaults first (through `consentStore.init`) and refuses to
184
150
  load a second time — it marks its own `<script>` element with an id derived from the container —
185
- but it runs after the bundle, which is exactly the window the head snippet closes. In `'basic'`
186
- mode it additionally withholds the element itself until `trackingGranted`, same as the head
187
- snippets.
188
- - **The default flipped to `'basic'`.** Code written before this mode existed called
189
- `googleTagHeadScript({ id })` / `gtmHeadScript({ id })` expecting the tag to load unconditionally.
190
- It now gates by default; pass `mode: 'advanced'` explicitly to keep the old behavior for a site
191
- that has made that call deliberately.
151
+ but it runs after the bundle, which is exactly the window the head snippet closes.
192
152
  - **A second `consent/default` after a tag has loaded can WIDEN what was already narrowed**, so the
193
153
  bootstrap is idempotent through a window flag. A page may therefore carry the call twice — once
194
154
  inline, once from the bundle that mounts the dialog — without harm. Do not defeat the flag.
@@ -198,8 +158,7 @@ cookie page does so when a Google tag is set. The strings are English data, not
198
158
  ## Depends On
199
159
 
200
160
  - `@owlmeans/consent` — `consentBootstrapScript`, `consentStore`, `ConsentOptions`,
201
- `ConsentService`, the category keys and the Consent Mode signal mapping, plus (for the `'basic'`
202
- gated mode) `consentGateScript` and `trackingGranted`
161
+ `ConsentService`, the category keys and the Consent Mode signal mapping
203
162
 
204
163
  ## Related
205
164
 
package/build/index.d.ts CHANGED
@@ -1,27 +1,9 @@
1
1
  import type { ConsentOptions, ConsentService } from '@owlmeans/consent';
2
- /** `'gtm'` container / `'gtag'` — see {@link GtmOptions.mode} / {@link GoogleTagOptions.mode}. */
3
- export type GoogleTagMode = 'basic' | 'advanced';
4
- /**
5
- * The platform default for {@link GtmOptions.mode} / {@link GoogleTagOptions.mode}.
6
- *
7
- * One named constant so flipping it later is a one-line change.
8
- *
9
- * - `'basic'` withholds the loader until a signal-bearing category is granted — which holds back
10
- * Google's own IP receipt too, not only what a granted tag may then do with it.
11
- * - `'advanced'` loads immediately with Consent Mode signals denied — today's only behavior, and
12
- * Google's own recommended default for its own conversion modeling.
13
- *
14
- * Default is `'basic'`: read it as the EU/DE worst-case reading of ePrivacy Art. 5(3) — no third
15
- * party receives a connection before consent.
16
- */
17
- export declare const GOOGLE_TAG_DEFAULT_MODE: GoogleTagMode;
18
2
  export interface GtmOptions extends ConsentOptions {
19
3
  /** Container id, e.g. `GTM-XXXXXXX`. */
20
4
  id: string;
21
5
  /** The queue name, when a page runs more than one container. */
22
6
  dataLayerName?: string;
23
- /** See {@link GOOGLE_TAG_DEFAULT_MODE}. Defaults to `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`). */
24
- mode?: GoogleTagMode;
25
7
  }
26
8
  /**
27
9
  * The inline `<head>` snippet, consent first and the container second.
@@ -33,20 +15,9 @@ export interface GtmOptions extends ConsentOptions {
33
15
  * the window that matters, and nothing in the page reports it.
34
16
  *
35
17
  * Emit this into the document head, above everything else, as an inline script.
36
- *
37
- * In `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`) the container is not requested at all until a
38
- * signal-bearing category is granted — {@link consentGateScript} wraps it, reading any stored
39
- * decision immediately and otherwise waiting for `@owlmeans/consent`'s `CONSENT_EVENT`. Pass
40
- * `mode: 'advanced'` for the original, unconditional load.
41
18
  */
42
19
  export declare const gtmHeadScript: (opts: GtmOptions) => string;
43
- /**
44
- * The `<noscript>` iframe, for the body.
45
- *
46
- * In `'basic'` mode this is an empty string: the whole point of gating is that an unauthenticated
47
- * `<noscript>` iframe would defeat it — a browser with JavaScript disabled cannot have granted
48
- * anything, so there is nothing lawful left to render. `'advanced'` keeps the frame.
49
- */
20
+ /** The `<noscript>` iframe, for the body. */
50
21
  export declare const gtmNoscriptFrame: (opts: GtmOptions) => string;
51
22
  /**
52
23
  * Load the container from script, for a host that cannot emit into its own head.
@@ -54,11 +25,6 @@ export declare const gtmNoscriptFrame: (opts: GtmOptions) => string;
54
25
  * The head snippet is strictly better and should be preferred; this exists for a single-page app
55
26
  * whose HTML is not ours to edit. It still pushes the defaults first, and it still refuses to load
56
27
  * a second time.
57
- *
58
- * In `'basic'` mode the container element is not appended until `trackingGranted` is true: either
59
- * the stored record already grants it, or — since this runs after the bundle mounted, past the
60
- * window the head snippet exists to close — a subscription to `consentStore` catches the first
61
- * update where it becomes true and unsubscribes. `'advanced'` appends it immediately, as before.
62
28
  */
63
29
  export declare const loadGtm: (opts: GtmOptions) => void;
64
30
  /**
@@ -80,8 +46,6 @@ export interface GoogleTagOptions extends ConsentOptions {
80
46
  * given another queue never hears a consent command. Leave it unset on a consent-gated page.
81
47
  */
82
48
  dataLayerName?: string;
83
- /** See {@link GOOGLE_TAG_DEFAULT_MODE}. Defaults to `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`). */
84
- mode?: GoogleTagMode;
85
49
  }
86
50
  /**
87
51
  * The one inline `<head>` script for a Google tag of any kind: the consent bootstrap FIRST, then
@@ -96,14 +60,6 @@ export interface GoogleTagOptions extends ConsentOptions {
96
60
  * An id that is not a loadable id ({@link isGoogleTagId}) yields the consent bootstrap alone — a
97
61
  * page must never lose its consent defaults because someone mistyped a tag. The result is safe to
98
62
  * place inline in HTML as-is: `</` and `<!--` inside any configured value are escaped.
99
- *
100
- * In `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`) the loader itself — the last piece — is wrapped in
101
- * `consentGateScript`: it runs immediately for a returning visitor whose stored record already
102
- * grants a signal-bearing category, and otherwise waits for `@owlmeans/consent`'s `CONSENT_EVENT`
103
- * before ever requesting the tag. The bootstrap and the redaction flags are unaffected and still
104
- * run unconditionally — Consent Mode's OWN denied-by-default signals are declared either way, so a
105
- * tag that later does load still starts from `ad_storage`/`analytics_storage` denied. Pass
106
- * `mode: 'advanced'` for the original, unconditional load.
107
63
  */
108
64
  export declare const googleTagHeadScript: (opts: GoogleTagOptions) => string;
109
65
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEvE,kGAAkG;AAClG,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,UAAU,CAAA;AAEhD;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,EAAE,aAAuB,CAAA;AAE7D,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8FAA8F;IAC9F,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAYD;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,aAAa,SAAU,UAAU,KAAG,MAShD,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,SAAU,UAAU,KAAG,MAOnD,CAAA;AAED;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,SAAU,UAAU,KAAG,IA0C1C,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,CAAA;AAc1C,sFAAsF;AACtF,eAAO,MAAM,aAAa,OAAQ,MAAM,KAAG,OACO,CAAA;AAElD,4EAA4E;AAC5E,eAAO,MAAM,aAAa,OAAQ,MAAM,KAAG,aAAa,GAAG,IACS,CAAA;AAEpE,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,8FAA8F;IAC9F,IAAI,CAAC,EAAE,aAAa,CAAA;CACrB;AAyDD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,mBAAmB,SAAU,gBAAgB,KAAG,MAe5D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAQlD,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAGpD,CAAA;AAkBF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,OAAQ,MAAM,KAAG,cAAc,EA8C5D,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAEvE,MAAM,WAAW,UAAW,SAAQ,cAAc;IAChD,wCAAwC;IACxC,EAAE,EAAE,MAAM,CAAA;IACV,gEAAgE;IAChE,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAYD;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,SAAU,UAAU,KAAG,MACoD,CAAA;AAErG,6CAA6C;AAC7C,eAAO,MAAM,gBAAgB,SAAU,UAAU,KAAG,MAEsB,CAAA;AAE1E;;;;;;GAMG;AACH,eAAO,MAAM,OAAO,SAAU,UAAU,KAAG,IAkB1C,CAAA;AAED;;;;GAIG;AACH,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,MAAM,CAAA;AAc1C,sFAAsF;AACtF,eAAO,MAAM,aAAa,OAAQ,MAAM,KAAG,OACO,CAAA;AAElD,4EAA4E;AAC5E,eAAO,MAAM,aAAa,OAAQ,MAAM,KAAG,aAAa,GAAG,IACS,CAAA;AAEpE,MAAM,WAAW,gBAAiB,SAAQ,cAAc;IACtD,4EAA4E;IAC5E,EAAE,EAAE,MAAM,CAAA;IACV;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB;AAyDD;;;;;;;;;;;;;GAaG;AACH,eAAO,MAAM,mBAAmB,SAAU,gBAAgB,KAAG,MAU5D,CAAA;AAED;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,sBAAsB,EAAE,SAAS,MAAM,EAQlD,CAAA;AAEF;;;GAGG;AACH,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAGpD,CAAA;AAkBF;;;;;;;;;GASG;AACH,eAAO,MAAM,iBAAiB,OAAQ,MAAM,KAAG,cAAc,EA8C5D,CAAA"}
package/build/index.js CHANGED
@@ -1,18 +1,4 @@
1
- import { CONSENT_ANALYTICS, CONSENT_MARKETING, consentBootstrapScript, consentGateScript, consentStore, trackingGranted, } from '@owlmeans/consent';
2
- /**
3
- * The platform default for {@link GtmOptions.mode} / {@link GoogleTagOptions.mode}.
4
- *
5
- * One named constant so flipping it later is a one-line change.
6
- *
7
- * - `'basic'` withholds the loader until a signal-bearing category is granted — which holds back
8
- * Google's own IP receipt too, not only what a granted tag may then do with it.
9
- * - `'advanced'` loads immediately with Consent Mode signals denied — today's only behavior, and
10
- * Google's own recommended default for its own conversion modeling.
11
- *
12
- * Default is `'basic'`: read it as the EU/DE worst-case reading of ePrivacy Art. 5(3) — no third
13
- * party receives a connection before consent.
14
- */
15
- export const GOOGLE_TAG_DEFAULT_MODE = 'basic';
1
+ import { CONSENT_ANALYTICS, CONSENT_MARKETING, consentBootstrapScript, consentStore, } from '@owlmeans/consent';
16
2
  /** Google's own container IIFE, the id and queue name injected as JSON data. */
17
3
  const gtmContainerScript = (layer, id) => `(function(w,d,s,l,i){w[l]=w[l]||[];` +
18
4
  `w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});` +
@@ -31,45 +17,17 @@ const gtmContainerScript = (layer, id) => `(function(w,d,s,l,i){w[l]=w[l]||[];`
31
17
  * the window that matters, and nothing in the page reports it.
32
18
  *
33
19
  * Emit this into the document head, above everything else, as an inline script.
34
- *
35
- * In `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`) the container is not requested at all until a
36
- * signal-bearing category is granted — {@link consentGateScript} wraps it, reading any stored
37
- * decision immediately and otherwise waiting for `@owlmeans/consent`'s `CONSENT_EVENT`. Pass
38
- * `mode: 'advanced'` for the original, unconditional load.
39
20
  */
40
- export const gtmHeadScript = (opts) => {
41
- const bootstrap = consentBootstrapScript(opts);
42
- const loader = gtmContainerScript(opts.dataLayerName ?? 'dataLayer', opts.id);
43
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) === 'basic') {
44
- return `${bootstrap};${consentGateScript(loader, opts)}`;
45
- }
46
- return `${bootstrap};${loader}`;
47
- };
48
- /**
49
- * The `<noscript>` iframe, for the body.
50
- *
51
- * In `'basic'` mode this is an empty string: the whole point of gating is that an unauthenticated
52
- * `<noscript>` iframe would defeat it — a browser with JavaScript disabled cannot have granted
53
- * anything, so there is nothing lawful left to render. `'advanced'` keeps the frame.
54
- */
55
- export const gtmNoscriptFrame = (opts) => {
56
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) === 'basic') {
57
- return '';
58
- }
59
- return `<iframe src="https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(opts.id)}"` +
60
- ` height="0" width="0" style="display:none;visibility:hidden"></iframe>`;
61
- };
21
+ export const gtmHeadScript = (opts) => `${consentBootstrapScript(opts)};${gtmContainerScript(opts.dataLayerName ?? 'dataLayer', opts.id)}`;
22
+ /** The `<noscript>` iframe, for the body. */
23
+ export const gtmNoscriptFrame = (opts) => `<iframe src="https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(opts.id)}"` +
24
+ ` height="0" width="0" style="display:none;visibility:hidden"></iframe>`;
62
25
  /**
63
26
  * Load the container from script, for a host that cannot emit into its own head.
64
27
  *
65
28
  * The head snippet is strictly better and should be preferred; this exists for a single-page app
66
29
  * whose HTML is not ours to edit. It still pushes the defaults first, and it still refuses to load
67
30
  * a second time.
68
- *
69
- * In `'basic'` mode the container element is not appended until `trackingGranted` is true: either
70
- * the stored record already grants it, or — since this runs after the bundle mounted, past the
71
- * window the head snippet exists to close — a subscription to `consentStore` catches the first
72
- * update where it becomes true and unsubscribes. `'advanced'` appends it immediately, as before.
73
31
  */
74
32
  export const loadGtm = (opts) => {
75
33
  if (typeof document === 'undefined') {
@@ -81,32 +39,13 @@ export const loadGtm = (opts) => {
81
39
  }
82
40
  // Defaults before the container, every time — the same rule the head snippet exists to keep.
83
41
  consentStore.init(opts);
84
- const inject = () => {
85
- if (document.getElementById(marker) != null) {
86
- return;
87
- }
88
- const layer = opts.dataLayerName ?? 'dataLayer';
89
- const script = document.createElement('script');
90
- script.id = marker;
91
- script.async = true;
92
- script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(opts.id)}`
93
- + (layer !== 'dataLayer' ? `&l=${encodeURIComponent(layer)}` : '');
94
- document.head.appendChild(script);
95
- };
96
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) !== 'basic') {
97
- inject();
98
- return;
99
- }
100
- if (trackingGranted(consentStore.get().record, opts.categories)) {
101
- inject();
102
- return;
103
- }
104
- const unsubscribe = consentStore.subscribe(state => {
105
- if (trackingGranted(state.record, opts.categories)) {
106
- unsubscribe();
107
- inject();
108
- }
109
- });
42
+ const layer = opts.dataLayerName ?? 'dataLayer';
43
+ const script = document.createElement('script');
44
+ script.id = marker;
45
+ script.async = true;
46
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(opts.id)}`
47
+ + (layer !== 'dataLayer' ? `&l=${encodeURIComponent(layer)}` : '');
48
+ document.head.appendChild(script);
110
49
  };
111
50
  /**
112
51
  * The id shapes Google issues today, and nothing else.
@@ -182,14 +121,6 @@ const gtagScript = (layer, id) => `(function(w,d,l,i){var m='owl-gtag-'+i;if(d.g
182
121
  * An id that is not a loadable id ({@link isGoogleTagId}) yields the consent bootstrap alone — a
183
122
  * page must never lose its consent defaults because someone mistyped a tag. The result is safe to
184
123
  * place inline in HTML as-is: `</` and `<!--` inside any configured value are escaped.
185
- *
186
- * In `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`) the loader itself — the last piece — is wrapped in
187
- * `consentGateScript`: it runs immediately for a returning visitor whose stored record already
188
- * grants a signal-bearing category, and otherwise waits for `@owlmeans/consent`'s `CONSENT_EVENT`
189
- * before ever requesting the tag. The bootstrap and the redaction flags are unaffected and still
190
- * run unconditionally — Consent Mode's OWN denied-by-default signals are declared either way, so a
191
- * tag that later does load still starts from `ad_storage`/`analytics_storage` denied. Pass
192
- * `mode: 'advanced'` for the original, unconditional load.
193
124
  */
194
125
  export const googleTagHeadScript = (opts) => {
195
126
  const bootstrap = consentBootstrapScript(opts);
@@ -199,11 +130,7 @@ export const googleTagHeadScript = (opts) => {
199
130
  }
200
131
  const layer = layerOf(opts);
201
132
  const loader = kind === 'gtm' ? gtmContainerScript(layer, opts.id) : gtagScript(layer, opts.id);
202
- const redaction = redactionScript(layer);
203
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) === 'basic') {
204
- return inlineSafe(`${bootstrap};${redaction};${consentGateScript(loader, opts)}`);
205
- }
206
- return inlineSafe(`${bootstrap};${redaction};${loader}`);
133
+ return inlineSafe(`${bootstrap};${redactionScript(layer)};${loader}`);
207
134
  };
208
135
  /**
209
136
  * The hosts a page running a Google tag has to allow in `script-src`, `connect-src` and
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@owlmeans/web-gtm",
3
- "version": "0.1.18-rc.26",
3
+ "version": "0.1.18-rc.27",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -22,7 +22,7 @@
22
22
  }
23
23
  },
24
24
  "dependencies": {
25
- "@owlmeans/consent": "^0.1.18-rc.6"
25
+ "@owlmeans/consent": "^0.1.18-rc.5"
26
26
  },
27
27
  "devDependencies": {
28
28
  "@owlmeans/dep-config": "workspace:*",
package/src/index.ts CHANGED
@@ -1,34 +1,13 @@
1
1
  import {
2
- CONSENT_ANALYTICS, CONSENT_MARKETING, consentBootstrapScript, consentGateScript, consentStore,
3
- trackingGranted,
2
+ CONSENT_ANALYTICS, CONSENT_MARKETING, consentBootstrapScript, consentStore,
4
3
  } from '@owlmeans/consent'
5
4
  import type { ConsentOptions, ConsentService } from '@owlmeans/consent'
6
5
 
7
- /** `'gtm'` container / `'gtag'` — see {@link GtmOptions.mode} / {@link GoogleTagOptions.mode}. */
8
- export type GoogleTagMode = 'basic' | 'advanced'
9
-
10
- /**
11
- * The platform default for {@link GtmOptions.mode} / {@link GoogleTagOptions.mode}.
12
- *
13
- * One named constant so flipping it later is a one-line change.
14
- *
15
- * - `'basic'` withholds the loader until a signal-bearing category is granted — which holds back
16
- * Google's own IP receipt too, not only what a granted tag may then do with it.
17
- * - `'advanced'` loads immediately with Consent Mode signals denied — today's only behavior, and
18
- * Google's own recommended default for its own conversion modeling.
19
- *
20
- * Default is `'basic'`: read it as the EU/DE worst-case reading of ePrivacy Art. 5(3) — no third
21
- * party receives a connection before consent.
22
- */
23
- export const GOOGLE_TAG_DEFAULT_MODE: GoogleTagMode = 'basic'
24
-
25
6
  export interface GtmOptions extends ConsentOptions {
26
7
  /** Container id, e.g. `GTM-XXXXXXX`. */
27
8
  id: string
28
9
  /** The queue name, when a page runs more than one container. */
29
10
  dataLayerName?: string
30
- /** See {@link GOOGLE_TAG_DEFAULT_MODE}. Defaults to `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`). */
31
- mode?: GoogleTagMode
32
11
  }
33
12
 
34
13
  /** Google's own container IIFE, the id and queue name injected as JSON data. */
@@ -51,38 +30,14 @@ const gtmContainerScript = (layer: string, id: string): string =>
51
30
  * the window that matters, and nothing in the page reports it.
52
31
  *
53
32
  * Emit this into the document head, above everything else, as an inline script.
54
- *
55
- * In `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`) the container is not requested at all until a
56
- * signal-bearing category is granted — {@link consentGateScript} wraps it, reading any stored
57
- * decision immediately and otherwise waiting for `@owlmeans/consent`'s `CONSENT_EVENT`. Pass
58
- * `mode: 'advanced'` for the original, unconditional load.
59
33
  */
60
- export const gtmHeadScript = (opts: GtmOptions): string => {
61
- const bootstrap = consentBootstrapScript(opts)
62
- const loader = gtmContainerScript(opts.dataLayerName ?? 'dataLayer', opts.id)
34
+ export const gtmHeadScript = (opts: GtmOptions): string =>
35
+ `${consentBootstrapScript(opts)};${gtmContainerScript(opts.dataLayerName ?? 'dataLayer', opts.id)}`
63
36
 
64
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) === 'basic') {
65
- return `${bootstrap};${consentGateScript(loader, opts)}`
66
- }
67
-
68
- return `${bootstrap};${loader}`
69
- }
70
-
71
- /**
72
- * The `<noscript>` iframe, for the body.
73
- *
74
- * In `'basic'` mode this is an empty string: the whole point of gating is that an unauthenticated
75
- * `<noscript>` iframe would defeat it — a browser with JavaScript disabled cannot have granted
76
- * anything, so there is nothing lawful left to render. `'advanced'` keeps the frame.
77
- */
78
- export const gtmNoscriptFrame = (opts: GtmOptions): string => {
79
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) === 'basic') {
80
- return ''
81
- }
82
-
83
- return `<iframe src="https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(opts.id)}"` +
84
- ` height="0" width="0" style="display:none;visibility:hidden"></iframe>`
85
- }
37
+ /** The `<noscript>` iframe, for the body. */
38
+ export const gtmNoscriptFrame = (opts: GtmOptions): string =>
39
+ `<iframe src="https://www.googletagmanager.com/ns.html?id=${encodeURIComponent(opts.id)}"` +
40
+ ` height="0" width="0" style="display:none;visibility:hidden"></iframe>`
86
41
 
87
42
  /**
88
43
  * Load the container from script, for a host that cannot emit into its own head.
@@ -90,11 +45,6 @@ export const gtmNoscriptFrame = (opts: GtmOptions): string => {
90
45
  * The head snippet is strictly better and should be preferred; this exists for a single-page app
91
46
  * whose HTML is not ours to edit. It still pushes the defaults first, and it still refuses to load
92
47
  * a second time.
93
- *
94
- * In `'basic'` mode the container element is not appended until `trackingGranted` is true: either
95
- * the stored record already grants it, or — since this runs after the bundle mounted, past the
96
- * window the head snippet exists to close — a subscription to `consentStore` catches the first
97
- * update where it becomes true and unsubscribes. `'advanced'` appends it immediately, as before.
98
48
  */
99
49
  export const loadGtm = (opts: GtmOptions): void => {
100
50
  if (typeof document === 'undefined') {
@@ -107,37 +57,13 @@ export const loadGtm = (opts: GtmOptions): void => {
107
57
  // Defaults before the container, every time — the same rule the head snippet exists to keep.
108
58
  consentStore.init(opts)
109
59
 
110
- const inject = (): void => {
111
- if (document.getElementById(marker) != null) {
112
- return
113
- }
114
- const layer = opts.dataLayerName ?? 'dataLayer'
115
- const script = document.createElement('script')
116
- script.id = marker
117
- script.async = true
118
- script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(opts.id)}`
119
- + (layer !== 'dataLayer' ? `&l=${encodeURIComponent(layer)}` : '')
120
- document.head.appendChild(script)
121
- }
122
-
123
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) !== 'basic') {
124
- inject()
125
-
126
- return
127
- }
128
-
129
- if (trackingGranted(consentStore.get().record, opts.categories)) {
130
- inject()
131
-
132
- return
133
- }
134
-
135
- const unsubscribe = consentStore.subscribe(state => {
136
- if (trackingGranted(state.record, opts.categories)) {
137
- unsubscribe()
138
- inject()
139
- }
140
- })
60
+ const layer = opts.dataLayerName ?? 'dataLayer'
61
+ const script = document.createElement('script')
62
+ script.id = marker
63
+ script.async = true
64
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(opts.id)}`
65
+ + (layer !== 'dataLayer' ? `&l=${encodeURIComponent(layer)}` : '')
66
+ document.head.appendChild(script)
141
67
  }
142
68
 
143
69
  /**
@@ -176,8 +102,6 @@ export interface GoogleTagOptions extends ConsentOptions {
176
102
  * given another queue never hears a consent command. Leave it unset on a consent-gated page.
177
103
  */
178
104
  dataLayerName?: string
179
- /** See {@link GOOGLE_TAG_DEFAULT_MODE}. Defaults to `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`). */
180
- mode?: GoogleTagMode
181
105
  }
182
106
 
183
107
  /** A queue name has to be a plain identifier: it becomes `window[name]` in the emitted script. */
@@ -248,14 +172,6 @@ const gtagScript = (layer: string, id: string): string =>
248
172
  * An id that is not a loadable id ({@link isGoogleTagId}) yields the consent bootstrap alone — a
249
173
  * page must never lose its consent defaults because someone mistyped a tag. The result is safe to
250
174
  * place inline in HTML as-is: `</` and `<!--` inside any configured value are escaped.
251
- *
252
- * In `GOOGLE_TAG_DEFAULT_MODE` (`'basic'`) the loader itself — the last piece — is wrapped in
253
- * `consentGateScript`: it runs immediately for a returning visitor whose stored record already
254
- * grants a signal-bearing category, and otherwise waits for `@owlmeans/consent`'s `CONSENT_EVENT`
255
- * before ever requesting the tag. The bootstrap and the redaction flags are unaffected and still
256
- * run unconditionally — Consent Mode's OWN denied-by-default signals are declared either way, so a
257
- * tag that later does load still starts from `ad_storage`/`analytics_storage` denied. Pass
258
- * `mode: 'advanced'` for the original, unconditional load.
259
175
  */
260
176
  export const googleTagHeadScript = (opts: GoogleTagOptions): string => {
261
177
  const bootstrap = consentBootstrapScript(opts)
@@ -265,13 +181,8 @@ export const googleTagHeadScript = (opts: GoogleTagOptions): string => {
265
181
  }
266
182
  const layer = layerOf(opts)
267
183
  const loader = kind === 'gtm' ? gtmContainerScript(layer, opts.id) : gtagScript(layer, opts.id)
268
- const redaction = redactionScript(layer)
269
-
270
- if ((opts.mode ?? GOOGLE_TAG_DEFAULT_MODE) === 'basic') {
271
- return inlineSafe(`${bootstrap};${redaction};${consentGateScript(loader, opts)}`)
272
- }
273
184
 
274
- return inlineSafe(`${bootstrap};${redaction};${loader}`)
185
+ return inlineSafe(`${bootstrap};${redactionScript(layer)};${loader}`)
275
186
  }
276
187
 
277
188
  /**
@@ -1,17 +1,17 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
- import { CONSENT_EVENT, CONSENT_KEY } from '@owlmeans/consent'
2
+ import { CONSENT_KEY } from '@owlmeans/consent'
3
3
  import { gtmHeadScript, gtmNoscriptFrame } from '../src/index.js'
4
4
 
5
5
  const ID = 'GTM-TESTID1'
6
6
 
7
- describe('@owlmeans/web-gtm — the head snippet ("advanced" mode — the original, unconditional load)', () => {
7
+ describe('@owlmeans/web-gtm — the head snippet', () => {
8
8
  test('the consent defaults are declared BEFORE the container is requested', async () => {
9
9
  // The entire reason this package exists. Consent Mode decides what a tag may do from the state
10
10
  // present when the container loads, so a `consent/default` that arrives after `gtm.js` leaves
11
11
  // the page unconfigured for the window that matters — and nothing reports it. Asserted on
12
12
  // offsets rather than on presence, because both strings are always present and only the order
13
13
  // has ever been wrong.
14
- const script = gtmHeadScript({ id: ID, mode: 'advanced' })
14
+ const script = gtmHeadScript({ id: ID })
15
15
 
16
16
  const defaults = script.indexOf("'consent','default'")
17
17
  const container = script.indexOf('gtm.js')
@@ -22,7 +22,7 @@ describe('@owlmeans/web-gtm — the head snippet ("advanced" mode — the origin
22
22
  })
23
23
 
24
24
  test('it reads a stored decision, so a returning visitor is not treated as denied', async () => {
25
- const script = gtmHeadScript({ id: ID, mode: 'advanced' })
25
+ const script = gtmHeadScript({ id: ID })
26
26
 
27
27
  expect(script).toContain(JSON.stringify(CONSENT_KEY))
28
28
  expect(script).toContain("'consent','update'")
@@ -36,7 +36,7 @@ describe('@owlmeans/web-gtm — the head snippet ("advanced" mode — the origin
36
36
  // encoding is what keeps a stray quote from closing the literal and turning the rest of an id
37
37
  // into executable script — the value still appears verbatim, and that is fine; what matters is
38
38
  // that a quote inside it is ESCAPED rather than terminating the string.
39
- const script = gtmHeadScript({ id: `${ID}"+alert(1)+"`, dataLayerName: 'owlLayer', mode: 'advanced' })
39
+ const script = gtmHeadScript({ id: `${ID}"+alert(1)+"`, dataLayerName: 'owlLayer' })
40
40
 
41
41
  expect(script).toContain(JSON.stringify(`${ID}"+alert(1)+"`))
42
42
  expect(script).toContain(JSON.stringify('owlLayer'))
@@ -46,46 +46,20 @@ describe('@owlmeans/web-gtm — the head snippet ("advanced" mode — the origin
46
46
  })
47
47
 
48
48
  test('a custom queue name is passed to the container', async () => {
49
- expect(gtmHeadScript({ id: ID, dataLayerName: 'owlLayer', mode: 'advanced' }))
50
- .toContain("l!='dataLayer'")
51
- expect(gtmHeadScript({ id: ID, mode: 'advanced' })).toContain(JSON.stringify('dataLayer'))
52
- })
53
- })
54
-
55
- describe('@owlmeans/web-gtm — the head snippet ("basic" mode — the default)', () => {
56
- test('gtmHeadScript gates the container behind consentGateScript by default', async () => {
57
- const script = gtmHeadScript({ id: ID })
58
- const advanced = gtmHeadScript({ id: ID, mode: 'advanced' })
59
-
60
- // The container is still IN the script — it's the loader `consentGateScript` withholds, not a
61
- // fact removed from the page — but it now sits behind the gate's own listener wiring.
62
- expect(script).toContain('gtm.js')
63
- expect(script).toContain(JSON.stringify(CONSENT_EVENT))
64
- expect(script).toContain('addEventListener')
65
- expect(script).not.toEqual(advanced)
66
- })
67
-
68
- test('mode: "basic" is the same as omitting mode entirely', async () => {
69
- expect(gtmHeadScript({ id: ID })).toBe(gtmHeadScript({ id: ID, mode: 'basic' }))
49
+ expect(gtmHeadScript({ id: ID, dataLayerName: 'owlLayer' })).toContain("l!='dataLayer'")
50
+ expect(gtmHeadScript({ id: ID })).toContain(JSON.stringify('dataLayer'))
70
51
  })
71
52
  })
72
53
 
73
54
  describe('@owlmeans/web-gtm — the noscript frame', () => {
74
- test('"advanced" mode names the container and stays invisible', async () => {
75
- const frame = gtmNoscriptFrame({ id: ID, mode: 'advanced' })
55
+ test('it names the container and stays invisible', async () => {
56
+ const frame = gtmNoscriptFrame({ id: ID })
76
57
 
77
58
  expect(frame).toContain(`id=${ID}`)
78
59
  expect(frame).toContain('display:none')
79
60
  })
80
61
 
81
62
  test('an id is URL-encoded, so it cannot break out of the attribute', async () => {
82
- expect(gtmNoscriptFrame({ id: 'a"b', mode: 'advanced' })).not.toContain('a"b')
83
- })
84
-
85
- test('"basic" mode (the default) renders nothing at all', async () => {
86
- // The point of gating is defeated by an unauthenticated `<noscript>` iframe: a browser with
87
- // JavaScript disabled cannot have granted anything, so there is nothing lawful to frame.
88
- expect(gtmNoscriptFrame({ id: ID })).toBe('')
89
- expect(gtmNoscriptFrame({ id: ID, mode: 'basic' })).toBe('')
63
+ expect(gtmNoscriptFrame({ id: 'a"b' })).not.toContain('a"b')
90
64
  })
91
65
  })
package/tests/tag.spec.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- CONSENT_EVENT, CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES, consentBootstrapScript,
3
+ CONSENT_KEY, DEFAULT_CONSENT_CATEGORIES, consentBootstrapScript,
4
4
  } from '@owlmeans/consent'
5
5
  import type { ConsentCategory } from '@owlmeans/consent'
6
6
  import {
@@ -35,18 +35,8 @@ const browser = (stored?: { key?: string, record: Record<string, unknown> }) =>
35
35
  loaded.push(element)
36
36
  if (element.id != null) byId.set(element.id, element)
37
37
  }
38
- const listeners = new Map<string, ((event: unknown) => void)[]>()
39
- const win: Record<string, unknown> & {
40
- addEventListener: (type: string, cb: (event: unknown) => void) => void
41
- removeEventListener: (type: string, cb: (event: unknown) => void) => void
42
- } = {
38
+ const win: Record<string, unknown> = {
43
39
  localStorage: { getItem: (key: string) => storage.get(key) ?? null },
44
- addEventListener: (type, cb) => {
45
- listeners.set(type, [...(listeners.get(type) ?? []), cb])
46
- },
47
- removeEventListener: (type, cb) => {
48
- listeners.set(type, (listeners.get(type) ?? []).filter(listener => listener !== cb))
49
- },
50
40
  }
51
41
  const doc = {
52
42
  cookie: '',
@@ -63,13 +53,6 @@ const browser = (stored?: { key?: string, record: Record<string, unknown> }) =>
63
53
  // eslint-disable-next-line no-new-func
64
54
  new Function('window', 'document', script)(win, doc)
65
55
  },
66
- /** Fire a `CONSENT_EVENT` the way `applyConsent` does, for the gated ("basic" mode) loader. */
67
- grant: (record: Record<string, unknown>) => {
68
- for (const cb of listeners.get(CONSENT_EVENT) ?? []) {
69
- cb({ detail: { record } })
70
- }
71
- },
72
- listenerCount: (type = CONSENT_EVENT) => listeners.get(type)?.length ?? 0,
73
56
  /** The queue as `command:arg` strings; gtm.js's own object entry shows as `event:gtm.js`. */
74
57
  queue: (name = 'dataLayer'): string[] =>
75
58
  ((win[name] as unknown[] | undefined) ?? []).map(entry => {
@@ -125,9 +108,9 @@ describe('isGoogleTagId / googleTagKind', () => {
125
108
  })
126
109
  })
127
110
 
128
- describe('googleTagHeadScript — the order ("advanced" mode — the original, unconditional load)', () => {
111
+ describe('googleTagHeadScript — the order', () => {
129
112
  test('gtag.js: consent default, then redaction, then js/config, then the library', () => {
130
- const script = googleTagHeadScript({ id: 'G-ABCD1234', mode: 'advanced' })
113
+ const script = googleTagHeadScript({ id: 'G-ABCD1234' })
131
114
  const page = browser()
132
115
  page.run(script)
133
116
 
@@ -150,7 +133,7 @@ describe('googleTagHeadScript — the order ("advanced" mode — the original, u
150
133
 
151
134
  test('the redaction flags carry the right values', () => {
152
135
  const page = browser()
153
- page.run(googleTagHeadScript({ id: 'AW-123456789', mode: 'advanced' }))
136
+ page.run(googleTagHeadScript({ id: 'AW-123456789' }))
154
137
  const sets = ((page.win.dataLayer as ArrayLike<unknown>[]) ?? [])
155
138
  .map(entry => Array.from(entry))
156
139
  .filter(args => args[0] === 'set')
@@ -159,7 +142,7 @@ describe('googleTagHeadScript — the order ("advanced" mode — the original, u
159
142
  })
160
143
 
161
144
  test('Tag Manager: consent default, then redaction, then the container', () => {
162
- const script = googleTagHeadScript({ id: 'GTM-ABC1234', mode: 'advanced' })
145
+ const script = googleTagHeadScript({ id: 'GTM-ABC1234' })
163
146
  const page = browser()
164
147
  page.run(script)
165
148
 
@@ -189,7 +172,7 @@ describe('googleTagHeadScript — the order ("advanced" mode — the original, u
189
172
  test('running it twice configures the tag once', () => {
190
173
  // Two stampings or a hot reload must not double-count every page view.
191
174
  const page = browser()
192
- const script = googleTagHeadScript({ id: 'G-ABCD1234', mode: 'advanced' })
175
+ const script = googleTagHeadScript({ id: 'G-ABCD1234' })
193
176
  page.run(script)
194
177
  page.run(script)
195
178
 
@@ -200,19 +183,19 @@ describe('googleTagHeadScript — the order ("advanced" mode — the original, u
200
183
 
201
184
  test('window.gtag is published for application events, and never replaced', () => {
202
185
  const page = browser()
203
- page.run(googleTagHeadScript({ id: 'G-ABCD1234', mode: 'advanced' }))
186
+ page.run(googleTagHeadScript({ id: 'G-ABCD1234' }))
204
187
  expect(typeof page.win.gtag).toBe('function')
205
188
 
206
189
  const owned = () => undefined
207
190
  const other = browser()
208
191
  other.win.gtag = owned
209
- other.run(googleTagHeadScript({ id: 'G-ABCD1234', mode: 'advanced' }))
192
+ other.run(googleTagHeadScript({ id: 'G-ABCD1234' }))
210
193
  expect(other.win.gtag).toBe(owned)
211
194
  })
212
195
 
213
196
  test('a custom queue is used by the tag and passed to the library', () => {
214
197
  const page = browser()
215
- page.run(googleTagHeadScript({ id: 'G-ABCD1234', dataLayerName: 'owlLayer', mode: 'advanced' }))
198
+ page.run(googleTagHeadScript({ id: 'G-ABCD1234', dataLayerName: 'owlLayer' }))
216
199
 
217
200
  expect(page.queue('owlLayer')).toContain('config:G-ABCD1234')
218
201
  expect(page.loaded[0].src).toBe('https://www.googletagmanager.com/gtag/js?id=G-ABCD1234&l=owlLayer')
@@ -220,78 +203,23 @@ describe('googleTagHeadScript — the order ("advanced" mode — the original, u
220
203
 
221
204
  test('a queue name that is not an identifier falls back to dataLayer', () => {
222
205
  const page = browser()
223
- page.run(googleTagHeadScript({ id: 'G-ABCD1234', dataLayerName: 'a-b', mode: 'advanced' }))
206
+ page.run(googleTagHeadScript({ id: 'G-ABCD1234', dataLayerName: 'a-b' }))
224
207
 
225
208
  expect(page.queue()).toContain('config:G-ABCD1234')
226
209
  expect(page.loaded[0].src).toBe('https://www.googletagmanager.com/gtag/js?id=G-ABCD1234')
227
210
  })
228
211
  })
229
212
 
230
- describe('googleTagHeadScript — "basic" mode (the default)', () => {
231
- test('an invalid id still yields only the bootstrap, regardless of mode', () => {
232
- // A mistyped tag must never cost the page its consent defaults — in either mode, since an id
233
- // this invalid never reaches the gate at all.
213
+ describe('googleTagHeadScript — an invalid id', () => {
214
+ test('yields the consent bootstrap alone', () => {
215
+ // A mistyped tag must never cost the page its consent defaults.
234
216
  for (const id of ['', 'UA-1234-1', 'G-x"+alert(1)']) {
235
- for (const mode of ['basic', 'advanced'] as const) {
236
- const script = googleTagHeadScript({ id, mode })
217
+ const script = googleTagHeadScript({ id })
237
218
 
238
- expect(script).toBe(consentBootstrapScript({ id } as never))
239
- expect(script).not.toContain('googletagmanager')
240
- expect(script).not.toContain('ads_data_redaction')
241
- }
219
+ expect(script).toBe(consentBootstrapScript({ id } as never))
220
+ expect(script).not.toContain('googletagmanager')
221
+ expect(script).not.toContain('ads_data_redaction')
242
222
  }
243
- // And omitting `mode` altogether is the same as the default, `'basic'`.
244
- expect(googleTagHeadScript({ id: '' })).toBe(googleTagHeadScript({ id: '', mode: 'basic' }))
245
- })
246
-
247
- test('a valid id with no stored consent does not load the tag synchronously', () => {
248
- const page = browser()
249
- page.run(googleTagHeadScript({ id: 'G-ABCD1234' }))
250
-
251
- // The consent bootstrap and the redaction flags still run unconditionally — Consent Mode's
252
- // OWN denied-by-default signals are declared either way — but nothing asked for the tag itself.
253
- expect(page.queue()).toEqual(['consent:default', 'set:ads_data_redaction', 'set:url_passthrough'])
254
- expect(page.loaded).toHaveLength(0)
255
- expect(page.listenerCount()).toBe(1)
256
- })
257
-
258
- test('it loads once the visitor grants a signal-bearing category, and not before', () => {
259
- const page = browser()
260
- page.run(googleTagHeadScript({ id: 'G-ABCD1234' }))
261
-
262
- // Denying, or granting only a category with no Consent Mode signal, changes nothing.
263
- page.grant({ essential: true, analytics: false, marketing: false })
264
- expect(page.loaded).toHaveLength(0)
265
-
266
- page.grant({ essential: true, analytics: true, marketing: false })
267
- expect(page.loaded).toHaveLength(1)
268
- expect(page.queue().filter(entry => entry.startsWith('config:'))).toEqual(['config:G-ABCD1234'])
269
- expect(page.listenerCount()).toBe(0)
270
-
271
- // A later grant does nothing further — the listener already removed itself, and the loader's
272
- // own anti-double-load guard would refuse a second run in any case.
273
- page.grant({ essential: true, analytics: true, marketing: true })
274
- expect(page.loaded).toHaveLength(1)
275
- })
276
-
277
- test('a returning visitor with a stored grant loads immediately, with no listener left behind', () => {
278
- const page = browser({ record: { essential: true, analytics: true, marketing: false, v: 2 } })
279
- page.run(googleTagHeadScript({ id: 'G-ABCD1234' }))
280
-
281
- expect(page.loaded).toHaveLength(1)
282
- expect(page.queue().filter(entry => entry.startsWith('config:'))).toEqual(['config:G-ABCD1234'])
283
- expect(page.listenerCount()).toBe(0)
284
- })
285
-
286
- test('the Tag Manager container is gated the same way', () => {
287
- const page = browser()
288
- page.run(googleTagHeadScript({ id: 'GTM-ABC1234' }))
289
-
290
- expect(page.loaded).toHaveLength(0)
291
-
292
- page.grant({ essential: true, marketing: true })
293
- expect(page.loaded).toHaveLength(1)
294
- expect(page.loaded[0].src).toBe('https://www.googletagmanager.com/gtm.js?id=GTM-ABC1234')
295
223
  })
296
224
  })
297
225