@owlmeans/web-gtm 0.1.18-rc.35 → 0.1.18-rc.37
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 +187 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/web-gtm/SKILL.md +10 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,3 +1,189 @@
|
|
|
1
|
+
# @owlmeans/web-gtm
|
|
2
|
+
|
|
3
|
+
The Google tag half of OwlMeans cookie consent. It emits the inline `<head>` script for any Google
|
|
4
|
+
id — Tag Manager (`GTM-`), GA4 (`G-`), Google tag (`GT-`), Google Ads (`AW-`), Floodlight (`DC-`) —
|
|
5
|
+
with the Consent Mode defaults declared **before** the loader runs, plus the id validator, the CSP
|
|
6
|
+
host lists, the cookie-policy disclosure for a tag, the `<noscript>` frame and a script-side loader
|
|
7
|
+
for hosts that cannot edit their HTML. It produces strings and data, holds no state, and reads the
|
|
8
|
+
consent decision from `@owlmeans/consent`. A web app or static site uses it whenever it runs a Google
|
|
9
|
+
tag. It does not render the consent dialog (`@owlmeans/web-consent`) and has no event API — events
|
|
10
|
+
reach `dataLayer` through `@owlmeans/log` with `@owlmeans/web-log`'s `gtmAnalyticsPlugin`. The
|
|
11
|
+
`web-gtm` skill covers this package; the `consent` skill covers the model and ordering rule it
|
|
12
|
+
implements.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
bun add @owlmeans/web-gtm@^0.1.18-rc.37
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Concepts
|
|
21
|
+
|
|
22
|
+
- **Order** — a Google tag decides what it may do from the consent state present when it loads. A
|
|
23
|
+
React bundle always arrives too late, so the head script emits the consent bootstrap first and the
|
|
24
|
+
loader after it, as one inline script stamped from HTML.
|
|
25
|
+
- **Mode** — `'basic'` (the default, `GOOGLE_TAG_DEFAULT_MODE`) withholds the loader until a
|
|
26
|
+
signal-bearing, non-required category is granted, from storage or a later `CONSENT_EVENT`.
|
|
27
|
+
`'advanced'` loads immediately with Consent Mode signals denied.
|
|
28
|
+
- **Bootstrap** — `consentModeHelper.consentBootstrapScript(opts)` from `@owlmeans/consent`: declares
|
|
29
|
+
`consent/default` and applies a stored decision in the same turn. It runs in both modes.
|
|
30
|
+
- **Kind** — `'gtm'` (container, `gtm.js`) for `GTM-`; `'gtag'` (`gtag/js` + `js` + `config`) for
|
|
31
|
+
`G-`, `GT-`, `AW-`, `DC-`.
|
|
32
|
+
- **Disclosure** — `ConsentService` entries describing who receives data, derived from the id prefix
|
|
33
|
+
for `CookiePolicy`'s `services`.
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### 1. Stamp the head script from HTML
|
|
38
|
+
|
|
39
|
+
Stamp it first in `<head>`, from the HTML build — never from the application bundle. A Vite app does
|
|
40
|
+
it with a `transformIndexHtml` plugin:
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import type { Plugin } from 'vite'
|
|
44
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
45
|
+
|
|
46
|
+
export const googleTagPlugin = (id: string): Plugin => ({
|
|
47
|
+
name: 'google-tag',
|
|
48
|
+
transformIndexHtml: html => html.replace(
|
|
49
|
+
'<!-- google-tag -->',
|
|
50
|
+
`<script>${googleTagHelper.googleTagHeadScript({ id })}</script>`
|
|
51
|
+
),
|
|
52
|
+
})
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
An invalid or empty id yields the consent bootstrap alone, so the call is safe to make
|
|
56
|
+
unconditionally. The output escapes `</` and `<!--`, so it can be placed inline as-is.
|
|
57
|
+
|
|
58
|
+
### 2. Keep the consent options in parity
|
|
59
|
+
|
|
60
|
+
`GoogleTagOptions` extends `ConsentOptions`. Pass the same `categories`, `storageKey` and `linker`
|
|
61
|
+
here as to `CookieConsent`, or the snippet and the dialog disagree about what was asked.
|
|
62
|
+
|
|
63
|
+
```typescript
|
|
64
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
65
|
+
|
|
66
|
+
const head = googleTagHelper.googleTagHeadScript({
|
|
67
|
+
id: 'GTM-ABC1234',
|
|
68
|
+
categories: appCategories,
|
|
69
|
+
linker: { domains: ['owlmeans.com', 'owlmeans.pl'] },
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// a site that has decided to load before consent, with signals denied
|
|
73
|
+
const eager = googleTagHelper.googleTagHeadScript({ id: 'G-ABCD1234', mode: 'advanced' })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. Validate the id where it is stored
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
80
|
+
|
|
81
|
+
if (!googleTagHelper.isGoogleTagId(form.googleTag)) {
|
|
82
|
+
throw new Error('invalid-google-tag')
|
|
83
|
+
}
|
|
84
|
+
googleTagHelper.googleTagKind('GTM-ABC1234') // 'gtm'
|
|
85
|
+
googleTagHelper.googleTagKind('AW-123456789') // 'gtag'
|
|
86
|
+
googleTagHelper.googleTagKind('UA-1234-1') // null — Universal Analytics is retired
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### 4. Allow the tag in the CSP
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
import { GOOGLE_TAG_CSP_SOURCES, GOOGLE_TAG_FRAME_SOURCES } from '@owlmeans/web-gtm'
|
|
93
|
+
|
|
94
|
+
const csp = [
|
|
95
|
+
`script-src 'self' ${scriptHash} ${GOOGLE_TAG_CSP_SOURCES.join(' ')}`,
|
|
96
|
+
`connect-src 'self' ${GOOGLE_TAG_CSP_SOURCES.join(' ')}`,
|
|
97
|
+
`img-src 'self' data: ${GOOGLE_TAG_CSP_SOURCES.join(' ')}`,
|
|
98
|
+
`frame-src ${GOOGLE_TAG_FRAME_SOURCES.join(' ')}`,
|
|
99
|
+
].join('; ')
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### 5. Disclose the tag on the cookie policy
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
import { CookiePolicy } from '@owlmeans/web-consent'
|
|
106
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
107
|
+
|
|
108
|
+
<CookiePolicy services={googleTagHelper.googleTagServices(googleTag)} privacyHref="/legal/privacy" />
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### 6. Load from script when the HTML is not yours
|
|
112
|
+
|
|
113
|
+
For a single-page app that cannot edit its own head. It calls `consentStore.init(opts)` first,
|
|
114
|
+
refuses to load twice, gates the same way in `'basic'` mode, and does nothing without a `document`.
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
import { googleTagHelper } from '@owlmeans/web-gtm'
|
|
118
|
+
|
|
119
|
+
googleTagHelper.loadGtm({ id: 'GTM-ABC1234', categories: appCategories })
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
## API
|
|
123
|
+
|
|
124
|
+
### `googleTagHelper` (`GoogleTagHelper`, built by `createGoogleTagHelper()`)
|
|
125
|
+
|
|
126
|
+
| Member | Returns | Purpose |
|
|
127
|
+
|---|---|---|
|
|
128
|
+
| `googleTagHeadScript(opts: GoogleTagOptions)` | `string` | Bootstrap, then `ads_data_redaction` on and `url_passthrough` off, then the loader for the id's kind (gated in `'basic'`); HTML-inline-safe. **The default for new code** |
|
|
129
|
+
| `gtmHeadScript(opts: GtmOptions)` | `string` | Older container-only form: bootstrap then `gtm.js`; gated the same way; does not escape `</` |
|
|
130
|
+
| `gtmNoscriptFrame(opts: GtmOptions)` | `string` | Hidden `<noscript>` iframe for `<body>`; `''` in `'basic'` mode |
|
|
131
|
+
| `loadGtm(opts: GtmOptions)` | `void` | Script-side container load (fallback) |
|
|
132
|
+
| `isGoogleTagId(id)` | `boolean` | Strict id check: `GTM-` + 4–12, or `G-`/`GT-`/`AW-`/`DC-` + 4–16 upper-case letters and digits |
|
|
133
|
+
| `googleTagKind(id)` | `'gtm' \| 'gtag' \| null` | Which loader an id takes |
|
|
134
|
+
| `googleTagServices(id)` | `ConsentService[]` | Cookie-policy disclosure read off the prefix |
|
|
135
|
+
|
|
136
|
+
`googleTagServices` returns: `G-` → Google Analytics (analytics; `_ga`, `_ga_<id>`); `AW-` → Google
|
|
137
|
+
Ads (marketing; `_gcl_au`, `_gcl_aw`); `DC-` → Floodlight (marketing; `_gcl_au`, `_gcl_dc`); `GTM-`
|
|
138
|
+
and `GT-` → one analytics and one advertising entry; an invalid id → `[]`.
|
|
139
|
+
|
|
140
|
+
### Constants
|
|
141
|
+
|
|
142
|
+
| Symbol | Value |
|
|
143
|
+
|---|---|
|
|
144
|
+
| `GOOGLE_TAG_DEFAULT_MODE` | `'basic'` |
|
|
145
|
+
| `GOOGLE_TAG_CSP_SOURCES` | frozen list for `script-src`, `connect-src`, `img-src`: `*.googletagmanager.com`, `*.google-analytics.com`, `*.g.doubleclick.net`, `ad.doubleclick.net`, `www.googleadservices.com`, `pagead2.googlesyndication.com`, `*.google.com` (all `https://`) |
|
|
146
|
+
| `GOOGLE_TAG_FRAME_SOURCES` | frozen list for `frame-src`: `https://www.googletagmanager.com`, `https://td.doubleclick.net` |
|
|
147
|
+
|
|
148
|
+
### Types
|
|
149
|
+
|
|
150
|
+
| Symbol | Shape |
|
|
151
|
+
|---|---|
|
|
152
|
+
| `GoogleTagOptions` | `ConsentOptions` + `{ id, dataLayerName?, mode? }` |
|
|
153
|
+
| `GtmOptions` | `ConsentOptions` + `{ id, dataLayerName?, mode? }` (container id) |
|
|
154
|
+
| `GoogleTagMode` | `'basic' \| 'advanced'` |
|
|
155
|
+
| `GoogleTagKind` | `'gtm' \| 'gtag'` |
|
|
156
|
+
| `GoogleTagHelper` | the helper interface above |
|
|
157
|
+
|
|
158
|
+
### Deprecated wrappers
|
|
159
|
+
|
|
160
|
+
`gtmHeadScript`, `gtmNoscriptFrame`, `isGoogleTagId`, `googleTagHeadScript`, `googleTagServices` —
|
|
161
|
+
top-level functions delegating to `googleTagHelper`. Use the helper members.
|
|
162
|
+
|
|
163
|
+
## Common pitfalls
|
|
164
|
+
|
|
165
|
+
- Loading the tag from the bundle (or with `loadGtm` where the head is editable) lets it run before
|
|
166
|
+
the consent defaults exist; stamp the head script from HTML.
|
|
167
|
+
- `dataLayerName`: Consent Mode always speaks on `window.dataLayer`, so a tag on another queue never
|
|
168
|
+
hears a consent command. Leave it unset on a consent-gated page.
|
|
169
|
+
- Code written before `'basic'` became the default and expects the tag to load unconditionally must
|
|
170
|
+
pass `mode: 'advanced'` explicitly.
|
|
171
|
+
- In `'basic'` mode a page counts as "tagged" in Tag Manager only after a consenting visitor loaded
|
|
172
|
+
it; a URL that redirects before the loader runs stays "not tagged".
|
|
173
|
+
- `isGoogleTagId` rejects lower case and `UA-` ids — normalise in the form, and validate where the
|
|
174
|
+
id is stored, not only where it is emitted.
|
|
175
|
+
- Under a hash-based CSP, Tag Manager Custom HTML tags and Custom JavaScript variables cannot run;
|
|
176
|
+
build containers from Google's built-in tag templates.
|
|
177
|
+
- Never build the snippet or the noscript frame by hand — the helpers JSON- or URL-encode the id and
|
|
178
|
+
queue name.
|
|
179
|
+
|
|
180
|
+
## Related packages
|
|
181
|
+
|
|
182
|
+
- [`@owlmeans/consent`](../consent) — the bootstrap, `consentGateScript`, `trackingGranted` and `consentStore`
|
|
183
|
+
- [`@owlmeans/web-consent`](../web-consent) — the dialog and `CookiePolicy` that consume `googleTagServices`
|
|
184
|
+
- [`@owlmeans/web-log`](../web-log) — consent-gated analytics events onto `dataLayer`
|
|
185
|
+
- [`@owlmeans/astro`](../astro) — composes these head strings for an Astro layout
|
|
186
|
+
|
|
1
187
|
<!-- owlmeans:agent-guidance:start -->
|
|
2
188
|
## Agent guidance
|
|
3
189
|
|
|
@@ -6,7 +192,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
|
|
|
6
192
|
your project's skill store (`.agents/skills/`):
|
|
7
193
|
|
|
8
194
|
```sh
|
|
9
|
-
npx @owlmeans/agent-skills@^0.1.18-rc.
|
|
195
|
+
npx @owlmeans/agent-skills@^0.1.18-rc.52
|
|
10
196
|
```
|
|
11
197
|
|
|
12
198
|
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-10-
|
|
4
|
+
"version": "0.1.18-rc.37",
|
|
5
|
+
"generatedAt": "2026-10-08T21:27:54.286Z",
|
|
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.37"` 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
|
|
@@ -39,6 +39,11 @@ constant so flipping the platform default later is a one-line change:
|
|
|
39
39
|
Read `'basic'`'s default as the EU/DE worst-case reading of ePrivacy Art. 5(3): no third party
|
|
40
40
|
receives a connection before consent.
|
|
41
41
|
|
|
42
|
+
**What `'basic'` does to a tag manager's coverage report.** A page is "Tagged" only after a consenting visitor
|
|
43
|
+
has loaded it; `'advanced'` is what makes every first view count. The report also records URLs it only saw as a
|
|
44
|
+
referrer, so a page the visitor was redirected AWAY from before the loader ran stays "Not tagged" — keep any
|
|
45
|
+
redirect off URLs that already name their locale.
|
|
46
|
+
|
|
42
47
|
`googleTagHelper.gtmNoscriptFrame` returns `''` in `'basic'` mode rather than an iframe — a browser with JavaScript
|
|
43
48
|
disabled cannot have granted anything, so an unauthenticated `<noscript>` frame would defeat the
|
|
44
49
|
whole point of gating. It keeps emitting the iframe in `'advanced'` mode, unchanged.
|
|
@@ -47,7 +52,10 @@ whole point of gating. It keeps emitting the iframe in `'advanced'` mode, unchan
|
|
|
47
52
|
only appends the container's `<script>` element once `consentModeHelper.trackingGranted(consentStore.get().record,
|
|
48
53
|
opts.categories)` is true — immediately if already granted, or via a one-shot `consentStore.subscribe`
|
|
49
54
|
that unsubscribes itself on the first update where it becomes true. The anti-double-load guard (the
|
|
50
|
-
element's own id) is unchanged.
|
|
55
|
+
element's own id) is unchanged. Give it the same `geo` the consent UI has: its `init` may run first,
|
|
56
|
+
and the store must know to locate the visitor rather than ask. An automatic decision (outside the
|
|
57
|
+
consent countries) is an ordinary grant here — the tag loads on it — while one past its age counts
|
|
58
|
+
as none, in the head gate as in the store, until the visitor is located again.
|
|
51
59
|
|
|
52
60
|
None of this is new state: it is one more consumer of `@owlmeans/consent`'s existing surface —
|
|
53
61
|
`consentModeHelper.trackingGranted(record, categories)` (whether a stored/applied record grants a signal-bearing,
|
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.37",
|
|
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.12"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@owlmeans/dep-config": "workspace:*",
|