@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 +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-gtm/SKILL.md +13 -54
- package/build/index.d.ts +1 -45
- package/build/index.d.ts.map +1 -1
- package/build/index.js +13 -86
- package/build/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +15 -104
- package/tests/head.spec.ts +10 -36
- package/tests/tag.spec.ts +18 -90
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.
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/web-gtm",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
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.
|
|
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
|
|
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
|
|
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`
|
|
70
|
-
| `gtmNoscriptFrame(opts)` | The hidden `<noscript>` iframe, for the top of `<body>`
|
|
71
|
-
| `loadGtm(opts)` | Script-side container load, for a host that cannot emit into its own head
|
|
72
|
-
| `GtmOptions` | `ConsentOptions` plus `id` (e.g. `GTM-XXXXXXX`)
|
|
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.
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
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.
|
|
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
|
|
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
|
/**
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
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,
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,
|
|
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.
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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};${
|
|
185
|
+
return inlineSafe(`${bootstrap};${redactionScript(layer)};${loader}`)
|
|
275
186
|
}
|
|
276
187
|
|
|
277
188
|
/**
|
package/tests/head.spec.ts
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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'
|
|
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'
|
|
50
|
-
|
|
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('
|
|
75
|
-
const frame = gtmNoscriptFrame({ id: ID
|
|
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'
|
|
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
|
-
|
|
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
|
|
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
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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 —
|
|
231
|
-
test('
|
|
232
|
-
// A mistyped tag must never cost the page its consent defaults
|
|
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
|
-
|
|
236
|
-
const script = googleTagHeadScript({ id, mode })
|
|
217
|
+
const script = googleTagHeadScript({ id })
|
|
237
218
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
|