wagtail-google-tag 0.1.0__tar.gz
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.
- wagtail_google_tag-0.1.0/PKG-INFO +222 -0
- wagtail_google_tag-0.1.0/README.md +190 -0
- wagtail_google_tag-0.1.0/pyproject.toml +67 -0
- wagtail_google_tag-0.1.0/pyproject.toml.orig +75 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/__init__.py +2 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/apps.py +12 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/checks.py +15 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/migrations/0001_initial.py +101 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/migrations/__init__.py +0 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/models.py +97 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/static/wagtail_google_tag/js/google-tag.js +97 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/tag.py +113 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/templates/wagtail_google_tag/google_tag.html +2 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/templatetags/__init__.py +0 -0
- wagtail_google_tag-0.1.0/src/wagtail_google_tag/templatetags/wagtail_google_tag.py +15 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wagtail-google-tag
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Google tag (GA4) for Wagtail, set up in the Wagtail admin: per-site settings, Consent Mode defaults, page data and click events.
|
|
5
|
+
License-Expression: BSD-3-Clause
|
|
6
|
+
Classifier: Development Status :: 3 - Alpha
|
|
7
|
+
Classifier: Intended Audience :: Developers
|
|
8
|
+
Classifier: Operating System :: OS Independent
|
|
9
|
+
Classifier: Programming Language :: Python
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
16
|
+
Classifier: Framework :: Django
|
|
17
|
+
Classifier: Framework :: Django :: 5.2
|
|
18
|
+
Classifier: Framework :: Django :: 6.0
|
|
19
|
+
Classifier: Framework :: Django :: 6.1
|
|
20
|
+
Classifier: Framework :: Wagtail
|
|
21
|
+
Classifier: Framework :: Wagtail :: 7
|
|
22
|
+
Classifier: Framework :: Wagtail :: 8
|
|
23
|
+
Requires-Dist: django>=5.2
|
|
24
|
+
Requires-Dist: wagtail>=7.4
|
|
25
|
+
Requires-Python: >=3.11
|
|
26
|
+
Project-URL: Homepage, https://github.com/Limitless-Horizons/wagtail-google-tag
|
|
27
|
+
Project-URL: Repository, https://github.com/Limitless-Horizons/wagtail-google-tag
|
|
28
|
+
Project-URL: Changelog, https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Documentation, https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/README.md
|
|
30
|
+
Project-URL: Issues, https://github.com/Limitless-Horizons/wagtail-google-tag/issues
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# Wagtail Google tag
|
|
34
|
+
|
|
35
|
+
Google tag (GA4) for Wagtail. Goal is everything you need to do to set up GA, you do through the Wagtail admin: per-site settings, Consent Mode defaults, page data and click events.
|
|
36
|
+
|
|
37
|
+
Claude Code was used with oversight by the maintainer.
|
|
38
|
+
|
|
39
|
+
## What it is
|
|
40
|
+
|
|
41
|
+
Wagtail Google tag adds Google's tag for Google Analytics 4 to a Wagtail site. Each site's measurement ID and options are set in the Wagtail admin. The tag loads with Consent Mode defaults that keep visitors in Europe out of analytics cookies until they agree, and it stays out of previews and away from signed-in editors. Pages can add their own data to each page view, and links and buttons can send events with two HTML attributes.
|
|
42
|
+
|
|
43
|
+
It doesn't bring Google Analytics into the Wagtail admin: reports, key events and data retention stay in Google Analytics. It also doesn't load Google Tag Manager containers or send server-side events.
|
|
44
|
+
|
|
45
|
+
## Requirements
|
|
46
|
+
|
|
47
|
+
- Wagtail 7.4, 8.0 or 8.1, with the [Python and Django versions](https://docs.wagtail.org/en/stable/releases/upgrading.html#compatible-django-python-versions) each of them supports
|
|
48
|
+
- `wagtail.contrib.settings`, which comes with Wagtail
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
1. Install the package:
|
|
53
|
+
|
|
54
|
+
```sh
|
|
55
|
+
pip install wagtail-google-tag
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
2. Add it to `INSTALLED_APPS`, with `wagtail.contrib.settings`:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
INSTALLED_APPS = [
|
|
62
|
+
# ...
|
|
63
|
+
"wagtail.contrib.settings",
|
|
64
|
+
"wagtail_google_tag",
|
|
65
|
+
# ...
|
|
66
|
+
]
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
3. Create its table:
|
|
70
|
+
|
|
71
|
+
```sh
|
|
72
|
+
python manage.py migrate
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
4. Add the tag near the top of `<head>` in your base template:
|
|
76
|
+
|
|
77
|
+
```django
|
|
78
|
+
{% load wagtail_google_tag %}
|
|
79
|
+
<!doctype html>
|
|
80
|
+
<html>
|
|
81
|
+
<head>
|
|
82
|
+
<meta charset="utf-8">
|
|
83
|
+
{% google_tag %}
|
|
84
|
+
...
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
5. In the Wagtail admin, go to **Settings → Google tag** and enter your GA4 measurement ID. In Google Analytics, it's under **Admin → Data streams**.
|
|
88
|
+
|
|
89
|
+
## What you get out of the box
|
|
90
|
+
|
|
91
|
+
- A `page_view` for every page, with GA's built-in `content_group` set to the page type, such as "Blog page".
|
|
92
|
+
- Google Analytics' own automatic tracking (enhanced measurement), such as scrolls, outbound clicks and site search, as you set it up in Google Analytics.
|
|
93
|
+
- [Consent Mode](#consent) defaults: analytics denied in Europe until a visitor agrees, and Google's advertising signals denied everywhere unless your site uses Google Ads.
|
|
94
|
+
- A `contact_link_click` event when a visitor clicks an email or phone link. Google Analytics' automatic tracking doesn't cover these.
|
|
95
|
+
- Nothing sent from Wagtail previews, or for signed-in users such as editors.
|
|
96
|
+
|
|
97
|
+
## Usage
|
|
98
|
+
|
|
99
|
+
### Click events
|
|
100
|
+
|
|
101
|
+
Add `data-google-tag-event` to any link or button to send that event when it's clicked. Add parameters as JSON in `data-google-tag-params`:
|
|
102
|
+
|
|
103
|
+
```html
|
|
104
|
+
<a href="/pricing/"
|
|
105
|
+
data-google-tag-event="select_content"
|
|
106
|
+
data-google-tag-params='{"content_type": "cta", "content_id": "pricing-hero"}'>
|
|
107
|
+
See pricing
|
|
108
|
+
</a>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
A click sends one event. An email or phone link with its own `data-google-tag-event` sends only that event, not `contact_link_click` as well.
|
|
112
|
+
|
|
113
|
+
### Page data
|
|
114
|
+
|
|
115
|
+
A page model can supply the parameters for its page view with `get_google_tag_params()`. Start from the defaults to keep the content group:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from wagtail.models import Page
|
|
119
|
+
from wagtail_google_tag.tag import default_page_params
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class BlogPage(Page):
|
|
123
|
+
...
|
|
124
|
+
|
|
125
|
+
def get_google_tag_params(self):
|
|
126
|
+
params = default_page_params(self) # {"content_group": "Blog page"}
|
|
127
|
+
params["blog_category"] = self.category.name
|
|
128
|
+
return params
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
To see a parameter like `blog_category` in Google Analytics' reports, register it there as a custom dimension.
|
|
132
|
+
|
|
133
|
+
### From scripts
|
|
134
|
+
|
|
135
|
+
The tag defines `window.wagtailGoogleTag`. It's there on every page, and sends nothing when the tag isn't loaded, so scripts never need to check:
|
|
136
|
+
|
|
137
|
+
```js
|
|
138
|
+
window.wagtailGoogleTag.track("generate_lead", { method: "newsletter" });
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Templates without a Wagtail page
|
|
142
|
+
|
|
143
|
+
Pages that aren't Wagtail pages, such as a 404 page, can pass their own parameters:
|
|
144
|
+
|
|
145
|
+
```django
|
|
146
|
+
{% google_tag content_group="Not found" %}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
These are added to the page's own parameters, and win if both set the same one.
|
|
150
|
+
|
|
151
|
+
## Consent
|
|
152
|
+
|
|
153
|
+
Before a visitor has made a choice, Google's tag starts with these Consent Mode defaults:
|
|
154
|
+
|
|
155
|
+
| Visitors | Analytics | Advertising signals |
|
|
156
|
+
|---|---|---|
|
|
157
|
+
| In the European Economic Area, the UK and Switzerland | Denied | Denied |
|
|
158
|
+
| Everywhere else | Granted | Denied, unless "Site uses Google Ads" is on |
|
|
159
|
+
|
|
160
|
+
Google works out where a visitor is. Denied analytics means no analytics cookies: Google Analytics receives only cookieless pings. To start with analytics granted everywhere, turn off **Deny analytics by default in Europe**.
|
|
161
|
+
|
|
162
|
+
A consent banner updates these defaults with the visitor's choice, in either of two ways:
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
// Call the function
|
|
166
|
+
window.wagtailGoogleTag.consent({ analytics: true, marketing: false });
|
|
167
|
+
|
|
168
|
+
// Or send this event on document
|
|
169
|
+
document.dispatchEvent(
|
|
170
|
+
new CustomEvent("wagtail-cookie-consent:change", {
|
|
171
|
+
detail: { analytics: true, marketing: false },
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
`analytics` sets analytics storage. `marketing` sets the advertising signals, and only when **Site uses Google Ads** is on. A banner that runs on each page load once a visitor has chosen should send the choice each time. In Europe, Google's tag waits half a second for that update before it sends anything.
|
|
177
|
+
|
|
178
|
+
The event comes from [wagtail-cookie-consent](https://github.com/Limitless-Horizons/wagtail-cookie-consent), a companion package in development, but any banner can send it or call the function.
|
|
179
|
+
|
|
180
|
+
## Settings reference
|
|
181
|
+
|
|
182
|
+
Each site has its own settings, under **Settings → Google tag**.
|
|
183
|
+
|
|
184
|
+
| Setting | Default | What it does |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| Measurement ID | Blank | The site's GA4 measurement ID (`G-…`). Blank means no tag, unless the Django setting below supplies one. |
|
|
187
|
+
| Deny analytics by default in Europe | On | The Europe defaults in [Consent](#consent). |
|
|
188
|
+
| Site uses Google Ads | Off | When off, advertising signals are denied for every visitor. |
|
|
189
|
+
| Track signed-in users | Off | When off, visits by signed-in users aren't tracked. |
|
|
190
|
+
| Track email and phone links | On | Sends `contact_link_click` with `link_type` set to `email` or `phone`. |
|
|
191
|
+
| Debug mode | Off | Sends events to Google Analytics' DebugView, for testing. |
|
|
192
|
+
|
|
193
|
+
`WAGTAIL_GOOGLE_TAG_MEASUREMENT_ID` in your Django settings is a fallback, used by any site whose measurement ID is blank. It's useful for moving an existing tag over, or for setting the ID from an environment variable.
|
|
194
|
+
|
|
195
|
+
### Testing your setup
|
|
196
|
+
|
|
197
|
+
Turn on **Debug mode**, then visit your site in a private window, since your own signed-in visits aren't tracked. Your page views and events show up in Google Analytics under **Admin → DebugView**. Turn debug mode off when you're done.
|
|
198
|
+
|
|
199
|
+
### Content Security Policy
|
|
200
|
+
|
|
201
|
+
If your site has a Content Security Policy, allow Google's tag: `https://www.googletagmanager.com` in `script-src`, and Google Analytics' collection domains, such as `https://*.google-analytics.com`, in `connect-src`. The package itself uses no inline scripts.
|
|
202
|
+
|
|
203
|
+
## Privacy notes
|
|
204
|
+
|
|
205
|
+
The package sends no personal data. Email and phone link events carry only `link_type`, never the address or number. Google's own rules forbid personal data in Google Analytics, so keep it out of your own event parameters too.
|
|
206
|
+
|
|
207
|
+
Some things are set in Google Analytics, not here:
|
|
208
|
+
|
|
209
|
+
- How long data is kept (Google's default is two months)
|
|
210
|
+
- Which events count as key events, such as `generate_lead`
|
|
211
|
+
- Custom dimensions for your own parameters
|
|
212
|
+
- Enhanced measurement: scrolls, outbound clicks, site search and the rest
|
|
213
|
+
|
|
214
|
+
## Links
|
|
215
|
+
|
|
216
|
+
- [Changelog](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CHANGELOG.md)
|
|
217
|
+
- [Contributing](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CONTRIBUTING.md)
|
|
218
|
+
- [Security policy](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/SECURITY.md)
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
BSD-3-Clause. See [LICENSE](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/LICENSE).
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Wagtail Google tag
|
|
2
|
+
|
|
3
|
+
Google tag (GA4) for Wagtail. Goal is everything you need to do to set up GA, you do through the Wagtail admin: per-site settings, Consent Mode defaults, page data and click events.
|
|
4
|
+
|
|
5
|
+
Claude Code was used with oversight by the maintainer.
|
|
6
|
+
|
|
7
|
+
## What it is
|
|
8
|
+
|
|
9
|
+
Wagtail Google tag adds Google's tag for Google Analytics 4 to a Wagtail site. Each site's measurement ID and options are set in the Wagtail admin. The tag loads with Consent Mode defaults that keep visitors in Europe out of analytics cookies until they agree, and it stays out of previews and away from signed-in editors. Pages can add their own data to each page view, and links and buttons can send events with two HTML attributes.
|
|
10
|
+
|
|
11
|
+
It doesn't bring Google Analytics into the Wagtail admin: reports, key events and data retention stay in Google Analytics. It also doesn't load Google Tag Manager containers or send server-side events.
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Wagtail 7.4, 8.0 or 8.1, with the [Python and Django versions](https://docs.wagtail.org/en/stable/releases/upgrading.html#compatible-django-python-versions) each of them supports
|
|
16
|
+
- `wagtail.contrib.settings`, which comes with Wagtail
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
1. Install the package:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
pip install wagtail-google-tag
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
2. Add it to `INSTALLED_APPS`, with `wagtail.contrib.settings`:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
INSTALLED_APPS = [
|
|
30
|
+
# ...
|
|
31
|
+
"wagtail.contrib.settings",
|
|
32
|
+
"wagtail_google_tag",
|
|
33
|
+
# ...
|
|
34
|
+
]
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
3. Create its table:
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
python manage.py migrate
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
4. Add the tag near the top of `<head>` in your base template:
|
|
44
|
+
|
|
45
|
+
```django
|
|
46
|
+
{% load wagtail_google_tag %}
|
|
47
|
+
<!doctype html>
|
|
48
|
+
<html>
|
|
49
|
+
<head>
|
|
50
|
+
<meta charset="utf-8">
|
|
51
|
+
{% google_tag %}
|
|
52
|
+
...
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
5. In the Wagtail admin, go to **Settings → Google tag** and enter your GA4 measurement ID. In Google Analytics, it's under **Admin → Data streams**.
|
|
56
|
+
|
|
57
|
+
## What you get out of the box
|
|
58
|
+
|
|
59
|
+
- A `page_view` for every page, with GA's built-in `content_group` set to the page type, such as "Blog page".
|
|
60
|
+
- Google Analytics' own automatic tracking (enhanced measurement), such as scrolls, outbound clicks and site search, as you set it up in Google Analytics.
|
|
61
|
+
- [Consent Mode](#consent) defaults: analytics denied in Europe until a visitor agrees, and Google's advertising signals denied everywhere unless your site uses Google Ads.
|
|
62
|
+
- A `contact_link_click` event when a visitor clicks an email or phone link. Google Analytics' automatic tracking doesn't cover these.
|
|
63
|
+
- Nothing sent from Wagtail previews, or for signed-in users such as editors.
|
|
64
|
+
|
|
65
|
+
## Usage
|
|
66
|
+
|
|
67
|
+
### Click events
|
|
68
|
+
|
|
69
|
+
Add `data-google-tag-event` to any link or button to send that event when it's clicked. Add parameters as JSON in `data-google-tag-params`:
|
|
70
|
+
|
|
71
|
+
```html
|
|
72
|
+
<a href="/pricing/"
|
|
73
|
+
data-google-tag-event="select_content"
|
|
74
|
+
data-google-tag-params='{"content_type": "cta", "content_id": "pricing-hero"}'>
|
|
75
|
+
See pricing
|
|
76
|
+
</a>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A click sends one event. An email or phone link with its own `data-google-tag-event` sends only that event, not `contact_link_click` as well.
|
|
80
|
+
|
|
81
|
+
### Page data
|
|
82
|
+
|
|
83
|
+
A page model can supply the parameters for its page view with `get_google_tag_params()`. Start from the defaults to keep the content group:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from wagtail.models import Page
|
|
87
|
+
from wagtail_google_tag.tag import default_page_params
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class BlogPage(Page):
|
|
91
|
+
...
|
|
92
|
+
|
|
93
|
+
def get_google_tag_params(self):
|
|
94
|
+
params = default_page_params(self) # {"content_group": "Blog page"}
|
|
95
|
+
params["blog_category"] = self.category.name
|
|
96
|
+
return params
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
To see a parameter like `blog_category` in Google Analytics' reports, register it there as a custom dimension.
|
|
100
|
+
|
|
101
|
+
### From scripts
|
|
102
|
+
|
|
103
|
+
The tag defines `window.wagtailGoogleTag`. It's there on every page, and sends nothing when the tag isn't loaded, so scripts never need to check:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
window.wagtailGoogleTag.track("generate_lead", { method: "newsletter" });
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Templates without a Wagtail page
|
|
110
|
+
|
|
111
|
+
Pages that aren't Wagtail pages, such as a 404 page, can pass their own parameters:
|
|
112
|
+
|
|
113
|
+
```django
|
|
114
|
+
{% google_tag content_group="Not found" %}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
These are added to the page's own parameters, and win if both set the same one.
|
|
118
|
+
|
|
119
|
+
## Consent
|
|
120
|
+
|
|
121
|
+
Before a visitor has made a choice, Google's tag starts with these Consent Mode defaults:
|
|
122
|
+
|
|
123
|
+
| Visitors | Analytics | Advertising signals |
|
|
124
|
+
|---|---|---|
|
|
125
|
+
| In the European Economic Area, the UK and Switzerland | Denied | Denied |
|
|
126
|
+
| Everywhere else | Granted | Denied, unless "Site uses Google Ads" is on |
|
|
127
|
+
|
|
128
|
+
Google works out where a visitor is. Denied analytics means no analytics cookies: Google Analytics receives only cookieless pings. To start with analytics granted everywhere, turn off **Deny analytics by default in Europe**.
|
|
129
|
+
|
|
130
|
+
A consent banner updates these defaults with the visitor's choice, in either of two ways:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
// Call the function
|
|
134
|
+
window.wagtailGoogleTag.consent({ analytics: true, marketing: false });
|
|
135
|
+
|
|
136
|
+
// Or send this event on document
|
|
137
|
+
document.dispatchEvent(
|
|
138
|
+
new CustomEvent("wagtail-cookie-consent:change", {
|
|
139
|
+
detail: { analytics: true, marketing: false },
|
|
140
|
+
}),
|
|
141
|
+
);
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`analytics` sets analytics storage. `marketing` sets the advertising signals, and only when **Site uses Google Ads** is on. A banner that runs on each page load once a visitor has chosen should send the choice each time. In Europe, Google's tag waits half a second for that update before it sends anything.
|
|
145
|
+
|
|
146
|
+
The event comes from [wagtail-cookie-consent](https://github.com/Limitless-Horizons/wagtail-cookie-consent), a companion package in development, but any banner can send it or call the function.
|
|
147
|
+
|
|
148
|
+
## Settings reference
|
|
149
|
+
|
|
150
|
+
Each site has its own settings, under **Settings → Google tag**.
|
|
151
|
+
|
|
152
|
+
| Setting | Default | What it does |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| Measurement ID | Blank | The site's GA4 measurement ID (`G-…`). Blank means no tag, unless the Django setting below supplies one. |
|
|
155
|
+
| Deny analytics by default in Europe | On | The Europe defaults in [Consent](#consent). |
|
|
156
|
+
| Site uses Google Ads | Off | When off, advertising signals are denied for every visitor. |
|
|
157
|
+
| Track signed-in users | Off | When off, visits by signed-in users aren't tracked. |
|
|
158
|
+
| Track email and phone links | On | Sends `contact_link_click` with `link_type` set to `email` or `phone`. |
|
|
159
|
+
| Debug mode | Off | Sends events to Google Analytics' DebugView, for testing. |
|
|
160
|
+
|
|
161
|
+
`WAGTAIL_GOOGLE_TAG_MEASUREMENT_ID` in your Django settings is a fallback, used by any site whose measurement ID is blank. It's useful for moving an existing tag over, or for setting the ID from an environment variable.
|
|
162
|
+
|
|
163
|
+
### Testing your setup
|
|
164
|
+
|
|
165
|
+
Turn on **Debug mode**, then visit your site in a private window, since your own signed-in visits aren't tracked. Your page views and events show up in Google Analytics under **Admin → DebugView**. Turn debug mode off when you're done.
|
|
166
|
+
|
|
167
|
+
### Content Security Policy
|
|
168
|
+
|
|
169
|
+
If your site has a Content Security Policy, allow Google's tag: `https://www.googletagmanager.com` in `script-src`, and Google Analytics' collection domains, such as `https://*.google-analytics.com`, in `connect-src`. The package itself uses no inline scripts.
|
|
170
|
+
|
|
171
|
+
## Privacy notes
|
|
172
|
+
|
|
173
|
+
The package sends no personal data. Email and phone link events carry only `link_type`, never the address or number. Google's own rules forbid personal data in Google Analytics, so keep it out of your own event parameters too.
|
|
174
|
+
|
|
175
|
+
Some things are set in Google Analytics, not here:
|
|
176
|
+
|
|
177
|
+
- How long data is kept (Google's default is two months)
|
|
178
|
+
- Which events count as key events, such as `generate_lead`
|
|
179
|
+
- Custom dimensions for your own parameters
|
|
180
|
+
- Enhanced measurement: scrolls, outbound clicks, site search and the rest
|
|
181
|
+
|
|
182
|
+
## Links
|
|
183
|
+
|
|
184
|
+
- [Changelog](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CHANGELOG.md)
|
|
185
|
+
- [Contributing](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CONTRIBUTING.md)
|
|
186
|
+
- [Security policy](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/SECURITY.md)
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
BSD-3-Clause. See [LICENSE](https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/LICENSE).
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wagtail-google-tag"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Google tag (GA4) for Wagtail, set up in the Wagtail admin: per-site settings, Consent Mode defaults, page data and click events."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "BSD-3-Clause"
|
|
7
|
+
classifiers = [
|
|
8
|
+
"Development Status :: 3 - Alpha",
|
|
9
|
+
"Intended Audience :: Developers",
|
|
10
|
+
"Operating System :: OS Independent",
|
|
11
|
+
"Programming Language :: Python",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.11",
|
|
14
|
+
"Programming Language :: Python :: 3.12",
|
|
15
|
+
"Programming Language :: Python :: 3.13",
|
|
16
|
+
"Programming Language :: Python :: 3.14",
|
|
17
|
+
"Programming Language :: Python :: 3.15",
|
|
18
|
+
"Framework :: Django",
|
|
19
|
+
"Framework :: Django :: 5.2",
|
|
20
|
+
"Framework :: Django :: 6.0",
|
|
21
|
+
"Framework :: Django :: 6.1",
|
|
22
|
+
"Framework :: Wagtail",
|
|
23
|
+
"Framework :: Wagtail :: 7",
|
|
24
|
+
"Framework :: Wagtail :: 8",
|
|
25
|
+
]
|
|
26
|
+
requires-python = ">=3.11"
|
|
27
|
+
dependencies = [
|
|
28
|
+
"django>=5.2",
|
|
29
|
+
"wagtail>=7.4",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://github.com/Limitless-Horizons/wagtail-google-tag"
|
|
34
|
+
Repository = "https://github.com/Limitless-Horizons/wagtail-google-tag"
|
|
35
|
+
Changelog = "https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CHANGELOG.md"
|
|
36
|
+
Documentation = "https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/README.md"
|
|
37
|
+
Issues = "https://github.com/Limitless-Horizons/wagtail-google-tag/issues"
|
|
38
|
+
|
|
39
|
+
[dependency-groups]
|
|
40
|
+
dev = [
|
|
41
|
+
"dj-database-url>=3.1.2",
|
|
42
|
+
"pytest>=8.1.1",
|
|
43
|
+
"pytest-cov>=7.1.0",
|
|
44
|
+
"pytest-django>=4.12.0",
|
|
45
|
+
"ruff>=0.15.9",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["uv_build>=0.12.6,<0.13"]
|
|
50
|
+
build-backend = "uv_build"
|
|
51
|
+
|
|
52
|
+
[tool.uv.build-backend]
|
|
53
|
+
module-name = "wagtail_google_tag"
|
|
54
|
+
source-exclude = [
|
|
55
|
+
"wagtail_google_tag/static_src",
|
|
56
|
+
"wagtail_google_tag/test",
|
|
57
|
+
"testmanage.py",
|
|
58
|
+
".*",
|
|
59
|
+
"prettier.config.js",
|
|
60
|
+
"*.json",
|
|
61
|
+
"*.ini",
|
|
62
|
+
"*.yml",
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
DJANGO_SETTINGS_MODULE = "wagtail_google_tag.test.settings"
|
|
67
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "wagtail-google-tag"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Google tag (GA4) for Wagtail, set up in the Wagtail admin: per-site settings, Consent Mode defaults, page data and click events."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "BSD-3-Clause"
|
|
7
|
+
classifiers = [
|
|
8
|
+
# Update this based on your project's status.
|
|
9
|
+
# See: https://pypi.org/classifiers/
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Programming Language :: Python",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.11",
|
|
16
|
+
"Programming Language :: Python :: 3.12",
|
|
17
|
+
"Programming Language :: Python :: 3.13",
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
|
+
"Programming Language :: Python :: 3.15",
|
|
20
|
+
"Framework :: Django",
|
|
21
|
+
"Framework :: Django :: 5.2",
|
|
22
|
+
"Framework :: Django :: 6.0",
|
|
23
|
+
"Framework :: Django :: 6.1",
|
|
24
|
+
"Framework :: Wagtail",
|
|
25
|
+
"Framework :: Wagtail :: 7",
|
|
26
|
+
"Framework :: Wagtail :: 8",
|
|
27
|
+
]
|
|
28
|
+
# See: https://devguide.python.org/versions/#supported-versions
|
|
29
|
+
requires-python = ">=3.11"
|
|
30
|
+
dependencies = [
|
|
31
|
+
# https://www.djangoproject.com/download/#supported-versions
|
|
32
|
+
"django>=5.2",
|
|
33
|
+
# https://github.com/wagtail/wagtail/wiki/Release-schedule
|
|
34
|
+
# https://docs.wagtail.org/en/latest/releases/upgrading.html#compatible-django-python-versions
|
|
35
|
+
"wagtail>=7.4",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Homepage = "https://github.com/Limitless-Horizons/wagtail-google-tag"
|
|
40
|
+
Repository = "https://github.com/Limitless-Horizons/wagtail-google-tag"
|
|
41
|
+
Changelog = "https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/CHANGELOG.md"
|
|
42
|
+
Documentation = "https://github.com/Limitless-Horizons/wagtail-google-tag/blob/main/README.md"
|
|
43
|
+
Issues = "https://github.com/Limitless-Horizons/wagtail-google-tag/issues"
|
|
44
|
+
# Add funding link if needed.
|
|
45
|
+
# Funding = "https://github.com/Limitless-Horizons/wagtail-google-tag"
|
|
46
|
+
|
|
47
|
+
[dependency-groups]
|
|
48
|
+
dev = [
|
|
49
|
+
"dj-database-url>=3.1.2",
|
|
50
|
+
"pytest>=8.1.1",
|
|
51
|
+
"pytest-cov>=7.1.0",
|
|
52
|
+
"pytest-django>=4.12.0",
|
|
53
|
+
"ruff>=0.15.9",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
[build-system]
|
|
57
|
+
requires = ["uv_build>=0.12.6,<0.13"]
|
|
58
|
+
build-backend = "uv_build"
|
|
59
|
+
|
|
60
|
+
[tool.uv.build-backend]
|
|
61
|
+
module-name = "wagtail_google_tag"
|
|
62
|
+
source-exclude = [
|
|
63
|
+
"wagtail_google_tag/static_src",
|
|
64
|
+
"wagtail_google_tag/test",
|
|
65
|
+
"testmanage.py",
|
|
66
|
+
".*",
|
|
67
|
+
"prettier.config.js",
|
|
68
|
+
"*.json",
|
|
69
|
+
"*.ini",
|
|
70
|
+
"*.yml",
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
[tool.pytest.ini_options]
|
|
74
|
+
DJANGO_SETTINGS_MODULE = "wagtail_google_tag.test.settings"
|
|
75
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from django.apps import AppConfig
|
|
2
|
+
from django.utils.translation import gettext_lazy as _
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class WagtailGoogleTagAppConfig(AppConfig):
|
|
6
|
+
label = "wagtail_google_tag"
|
|
7
|
+
name = "wagtail_google_tag"
|
|
8
|
+
verbose_name = _("Google tag")
|
|
9
|
+
default_auto_field = "django.db.models.BigAutoField"
|
|
10
|
+
|
|
11
|
+
def ready(self):
|
|
12
|
+
from . import checks # noqa: F401
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.core.checks import Error, register
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@register()
|
|
6
|
+
def settings_app_installed(app_configs, **kwargs):
|
|
7
|
+
if apps.is_installed("wagtail.contrib.settings"):
|
|
8
|
+
return []
|
|
9
|
+
return [
|
|
10
|
+
Error(
|
|
11
|
+
"wagtail_google_tag needs wagtail.contrib.settings.",
|
|
12
|
+
hint='Add "wagtail.contrib.settings" to INSTALLED_APPS.',
|
|
13
|
+
id="wagtail_google_tag.E001",
|
|
14
|
+
)
|
|
15
|
+
]
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Generated by Django 5.2.19 on 2026-11-10 18:57
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
import django.core.validators
|
|
6
|
+
import django.db.models.deletion
|
|
7
|
+
|
|
8
|
+
from django.db import migrations, models
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Migration(migrations.Migration):
|
|
12
|
+
initial = True
|
|
13
|
+
|
|
14
|
+
dependencies = [
|
|
15
|
+
("wagtailcore", "0097_baselogentry_uuid_action_timestamp_indexes"),
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
operations = [
|
|
19
|
+
migrations.CreateModel(
|
|
20
|
+
name="GoogleTagSettings",
|
|
21
|
+
fields=[
|
|
22
|
+
(
|
|
23
|
+
"id",
|
|
24
|
+
models.BigAutoField(
|
|
25
|
+
auto_created=True,
|
|
26
|
+
primary_key=True,
|
|
27
|
+
serialize=False,
|
|
28
|
+
verbose_name="ID",
|
|
29
|
+
),
|
|
30
|
+
),
|
|
31
|
+
(
|
|
32
|
+
"measurement_id",
|
|
33
|
+
models.CharField(
|
|
34
|
+
blank=True,
|
|
35
|
+
help_text="The GA4 measurement ID for this site, like G-A1B2C3D4E5. In Google Analytics, it's under Admin, Data streams. Leave it blank for no tag, unless the site's configuration supplies a default.",
|
|
36
|
+
max_length=20,
|
|
37
|
+
validators=[
|
|
38
|
+
django.core.validators.RegexValidator(
|
|
39
|
+
"^G-[A-Z0-9]+$",
|
|
40
|
+
"Enter a GA4 measurement ID, like G-A1B2C3D4E5.",
|
|
41
|
+
flags=re.RegexFlag["IGNORECASE"],
|
|
42
|
+
)
|
|
43
|
+
],
|
|
44
|
+
verbose_name="measurement ID",
|
|
45
|
+
),
|
|
46
|
+
),
|
|
47
|
+
(
|
|
48
|
+
"deny_analytics_in_europe",
|
|
49
|
+
models.BooleanField(
|
|
50
|
+
default=True,
|
|
51
|
+
help_text="Visitors in the European Economic Area, the UK and Switzerland get no analytics cookies until they agree to them, for example through a consent banner.",
|
|
52
|
+
verbose_name="deny analytics by default in Europe",
|
|
53
|
+
),
|
|
54
|
+
),
|
|
55
|
+
(
|
|
56
|
+
"uses_google_ads",
|
|
57
|
+
models.BooleanField(
|
|
58
|
+
default=False,
|
|
59
|
+
help_text="When this is off, Google's advertising signals are denied for every visitor.",
|
|
60
|
+
verbose_name="site uses Google Ads",
|
|
61
|
+
),
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
"track_signed_in_users",
|
|
65
|
+
models.BooleanField(
|
|
66
|
+
default=False,
|
|
67
|
+
help_text="When this is off, visits by signed-in users, such as editors, are left out of the data.",
|
|
68
|
+
verbose_name="track signed-in users",
|
|
69
|
+
),
|
|
70
|
+
),
|
|
71
|
+
(
|
|
72
|
+
"track_contact_links",
|
|
73
|
+
models.BooleanField(
|
|
74
|
+
default=True,
|
|
75
|
+
help_text="Clicks on email and phone links send a contact_link_click event. The address or number itself is never sent.",
|
|
76
|
+
verbose_name="track email and phone links",
|
|
77
|
+
),
|
|
78
|
+
),
|
|
79
|
+
(
|
|
80
|
+
"debug_mode",
|
|
81
|
+
models.BooleanField(
|
|
82
|
+
default=False,
|
|
83
|
+
help_text="Sends events to DebugView in Google Analytics, for testing. Turn it off when you're done.",
|
|
84
|
+
verbose_name="debug mode",
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
(
|
|
88
|
+
"site",
|
|
89
|
+
models.OneToOneField(
|
|
90
|
+
editable=False,
|
|
91
|
+
on_delete=django.db.models.deletion.CASCADE,
|
|
92
|
+
to="wagtailcore.site",
|
|
93
|
+
),
|
|
94
|
+
),
|
|
95
|
+
],
|
|
96
|
+
options={
|
|
97
|
+
"verbose_name": "Google tag",
|
|
98
|
+
"verbose_name_plural": "Google tag",
|
|
99
|
+
},
|
|
100
|
+
),
|
|
101
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from django.core.validators import RegexValidator
|
|
4
|
+
from django.db import models
|
|
5
|
+
from django.utils.translation import gettext_lazy as _
|
|
6
|
+
from wagtail.admin.panels import FieldPanel, MultiFieldPanel
|
|
7
|
+
from wagtail.contrib.settings.models import BaseSiteSetting, register_setting
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
measurement_id_validator = RegexValidator(
|
|
11
|
+
r"^G-[A-Z0-9]+$",
|
|
12
|
+
_("Enter a GA4 measurement ID, like G-A1B2C3D4E5."),
|
|
13
|
+
flags=re.IGNORECASE,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@register_setting(icon="tag")
|
|
18
|
+
class GoogleTagSettings(BaseSiteSetting):
|
|
19
|
+
measurement_id = models.CharField(
|
|
20
|
+
_("measurement ID"),
|
|
21
|
+
max_length=20,
|
|
22
|
+
blank=True,
|
|
23
|
+
validators=[measurement_id_validator],
|
|
24
|
+
help_text=_(
|
|
25
|
+
"The GA4 measurement ID for this site, like G-A1B2C3D4E5. In Google "
|
|
26
|
+
"Analytics, it's under Admin, Data streams. Leave it blank for no tag, "
|
|
27
|
+
"unless the site's configuration supplies a default."
|
|
28
|
+
),
|
|
29
|
+
)
|
|
30
|
+
deny_analytics_in_europe = models.BooleanField(
|
|
31
|
+
_("deny analytics by default in Europe"),
|
|
32
|
+
default=True,
|
|
33
|
+
help_text=_(
|
|
34
|
+
"Visitors in the European Economic Area, the UK and Switzerland get no "
|
|
35
|
+
"analytics cookies until they agree to them, for example through a "
|
|
36
|
+
"consent banner."
|
|
37
|
+
),
|
|
38
|
+
)
|
|
39
|
+
uses_google_ads = models.BooleanField(
|
|
40
|
+
_("site uses Google Ads"),
|
|
41
|
+
default=False,
|
|
42
|
+
help_text=_(
|
|
43
|
+
"When this is off, Google's advertising signals are denied for every "
|
|
44
|
+
"visitor."
|
|
45
|
+
),
|
|
46
|
+
)
|
|
47
|
+
track_signed_in_users = models.BooleanField(
|
|
48
|
+
_("track signed-in users"),
|
|
49
|
+
default=False,
|
|
50
|
+
help_text=_(
|
|
51
|
+
"When this is off, visits by signed-in users, such as editors, are "
|
|
52
|
+
"left out of the data."
|
|
53
|
+
),
|
|
54
|
+
)
|
|
55
|
+
track_contact_links = models.BooleanField(
|
|
56
|
+
_("track email and phone links"),
|
|
57
|
+
default=True,
|
|
58
|
+
help_text=_(
|
|
59
|
+
"Clicks on email and phone links send a contact_link_click event. The "
|
|
60
|
+
"address or number itself is never sent."
|
|
61
|
+
),
|
|
62
|
+
)
|
|
63
|
+
debug_mode = models.BooleanField(
|
|
64
|
+
_("debug mode"),
|
|
65
|
+
default=False,
|
|
66
|
+
help_text=_(
|
|
67
|
+
"Sends events to DebugView in Google Analytics, for testing. Turn it "
|
|
68
|
+
"off when you're done."
|
|
69
|
+
),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
panels = [
|
|
73
|
+
FieldPanel("measurement_id"),
|
|
74
|
+
MultiFieldPanel(
|
|
75
|
+
[
|
|
76
|
+
FieldPanel("deny_analytics_in_europe"),
|
|
77
|
+
FieldPanel("uses_google_ads"),
|
|
78
|
+
],
|
|
79
|
+
heading=_("Consent"),
|
|
80
|
+
),
|
|
81
|
+
MultiFieldPanel(
|
|
82
|
+
[
|
|
83
|
+
FieldPanel("track_signed_in_users"),
|
|
84
|
+
FieldPanel("track_contact_links"),
|
|
85
|
+
FieldPanel("debug_mode"),
|
|
86
|
+
],
|
|
87
|
+
heading=_("Tracking"),
|
|
88
|
+
),
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
class Meta:
|
|
92
|
+
verbose_name = _("Google tag")
|
|
93
|
+
verbose_name_plural = _("Google tag")
|
|
94
|
+
|
|
95
|
+
def save(self, *args, **kwargs):
|
|
96
|
+
self.measurement_id = self.measurement_id.strip().upper()
|
|
97
|
+
super().save(*args, **kwargs)
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wagtail Google tag: loads Google's tag with Consent Mode defaults, and
|
|
3
|
+
* defines window.wagtailGoogleTag:
|
|
4
|
+
*
|
|
5
|
+
* - track(name, params) sends an event.
|
|
6
|
+
* - consent({ analytics, marketing }) updates Consent Mode with a visitor's
|
|
7
|
+
* choice. A "wagtail-cookie-consent:change" event on document, with the same
|
|
8
|
+
* object as its detail, does the same.
|
|
9
|
+
*
|
|
10
|
+
* Both do nothing when the tag isn't loaded (no measurement ID, a preview, or
|
|
11
|
+
* a signed-in user who isn't tracked), so pages never need to check.
|
|
12
|
+
*
|
|
13
|
+
* Clicks on elements with data-google-tag-event send that event, with
|
|
14
|
+
* parameters from data-google-tag-params (JSON). Clicks on email and phone
|
|
15
|
+
* links send contact_link_click, unless the link has its own event.
|
|
16
|
+
*/
|
|
17
|
+
(() => {
|
|
18
|
+
if (window.wagtailGoogleTag) return;
|
|
19
|
+
|
|
20
|
+
const configElement = document.getElementById('wagtail-google-tag-config');
|
|
21
|
+
const config = configElement ? JSON.parse(configElement.textContent) : {};
|
|
22
|
+
const enabled = Boolean(config.id);
|
|
23
|
+
|
|
24
|
+
window.dataLayer = window.dataLayer || [];
|
|
25
|
+
function gtag() {
|
|
26
|
+
// Google's tag reads the arguments object itself, not an array.
|
|
27
|
+
window.dataLayer.push(arguments);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (enabled) {
|
|
31
|
+
window.gtag = gtag;
|
|
32
|
+
config.consent.forEach((defaults) => gtag('consent', 'default', defaults));
|
|
33
|
+
gtag('js', new Date());
|
|
34
|
+
gtag('config', config.id, config.params);
|
|
35
|
+
|
|
36
|
+
const script = document.createElement('script');
|
|
37
|
+
script.async = true;
|
|
38
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${encodeURIComponent(config.id)}`;
|
|
39
|
+
document.head.appendChild(script);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const track = (name, params) => {
|
|
43
|
+
if (enabled && name) gtag('event', name, params || {});
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const consent = (choice) => {
|
|
47
|
+
if (!enabled || !choice) return;
|
|
48
|
+
const update = {};
|
|
49
|
+
if ('analytics' in choice) {
|
|
50
|
+
update.analytics_storage = choice.analytics ? 'granted' : 'denied';
|
|
51
|
+
}
|
|
52
|
+
// Ad signals stay denied unless the site uses Google Ads.
|
|
53
|
+
if (config.ads && 'marketing' in choice) {
|
|
54
|
+
const value = choice.marketing ? 'granted' : 'denied';
|
|
55
|
+
update.ad_storage = value;
|
|
56
|
+
update.ad_user_data = value;
|
|
57
|
+
update.ad_personalization = value;
|
|
58
|
+
}
|
|
59
|
+
if (Object.keys(update).length) gtag('consent', 'update', update);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
window.wagtailGoogleTag = { track, consent };
|
|
63
|
+
|
|
64
|
+
document.addEventListener('wagtail-cookie-consent:change', (event) => {
|
|
65
|
+
consent(event.detail);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const parseParams = (element) => {
|
|
69
|
+
const json = element.getAttribute('data-google-tag-params');
|
|
70
|
+
if (!json) return {};
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(json);
|
|
73
|
+
} catch {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
document.addEventListener('click', (event) => {
|
|
79
|
+
if (!enabled || !(event.target instanceof Element)) return;
|
|
80
|
+
|
|
81
|
+
const tagged = event.target.closest('[data-google-tag-event]');
|
|
82
|
+
if (tagged) {
|
|
83
|
+
track(tagged.getAttribute('data-google-tag-event'), parseParams(tagged));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!config.contactLinks) return;
|
|
88
|
+
const link = event.target.closest('a[href]');
|
|
89
|
+
if (!link) return;
|
|
90
|
+
// No address or number is sent: GA forbids personal data.
|
|
91
|
+
if (link.protocol === 'mailto:') {
|
|
92
|
+
track('contact_link_click', { link_type: 'email' });
|
|
93
|
+
} else if (link.protocol === 'tel:') {
|
|
94
|
+
track('contact_link_click', { link_type: 'phone' });
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
})();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""
|
|
2
|
+
What the {% google_tag %} template tag renders: whether Google's tag loads for
|
|
3
|
+
this request, and the configuration the browser script gets.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from django.conf import settings
|
|
7
|
+
from django.utils.text import capfirst
|
|
8
|
+
from wagtail.models import Page
|
|
9
|
+
|
|
10
|
+
from .models import GoogleTagSettings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# The European Economic Area (the EU, Iceland, Liechtenstein and Norway), the
|
|
14
|
+
# UK and Switzerland, as ISO 3166-1 codes for Consent Mode's "region".
|
|
15
|
+
EUROPE = (
|
|
16
|
+
# EU
|
|
17
|
+
"AT", "BE", "BG", "CY", "CZ", "DE", "DK", "EE", "ES", "FI", "FR", "GR", "HR",
|
|
18
|
+
"HU", "IE", "IT", "LT", "LU", "LV", "MT", "NL", "PL", "PT", "RO", "SE", "SI",
|
|
19
|
+
"SK",
|
|
20
|
+
# Rest of the EEA
|
|
21
|
+
"IS", "LI", "NO",
|
|
22
|
+
# UK and Switzerland
|
|
23
|
+
"GB", "CH",
|
|
24
|
+
) # fmt: skip
|
|
25
|
+
|
|
26
|
+
# How long Google's tag waits for a consent banner's update in Europe, in ms.
|
|
27
|
+
WAIT_FOR_UPDATE = 500
|
|
28
|
+
|
|
29
|
+
AD_SIGNALS = ("ad_storage", "ad_user_data", "ad_personalization")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_settings(request):
|
|
33
|
+
"""This site's settings, or the defaults when the request has no site."""
|
|
34
|
+
try:
|
|
35
|
+
return GoogleTagSettings.for_request(request)
|
|
36
|
+
except GoogleTagSettings.DoesNotExist:
|
|
37
|
+
return GoogleTagSettings()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_measurement_id(site_settings):
|
|
41
|
+
"""The site's measurement ID, or the WAGTAIL_GOOGLE_TAG_MEASUREMENT_ID
|
|
42
|
+
setting when the site has none."""
|
|
43
|
+
return site_settings.measurement_id or getattr(
|
|
44
|
+
settings, "WAGTAIL_GOOGLE_TAG_MEASUREMENT_ID", ""
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def default_page_params(page):
|
|
49
|
+
"""The page_view parameters for a page without get_google_tag_params():
|
|
50
|
+
its page type as GA's content group."""
|
|
51
|
+
model = page.specific_class or type(page)
|
|
52
|
+
return {"content_group": str(capfirst(model._meta.verbose_name))}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def page_params(page):
|
|
56
|
+
get_params = getattr(page, "get_google_tag_params", None)
|
|
57
|
+
if get_params is None:
|
|
58
|
+
return default_page_params(page)
|
|
59
|
+
return dict(get_params())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def consent_defaults(site_settings):
|
|
63
|
+
"""Consent Mode defaults: analytics granted, ad signals denied unless the
|
|
64
|
+
site uses Google Ads, and everything denied in Europe if the site says so.
|
|
65
|
+
Google applies the most specific region that matches the visitor."""
|
|
66
|
+
ads = "granted" if site_settings.uses_google_ads else "denied"
|
|
67
|
+
defaults = [{"analytics_storage": "granted", **dict.fromkeys(AD_SIGNALS, ads)}]
|
|
68
|
+
if site_settings.deny_analytics_in_europe:
|
|
69
|
+
defaults.append(
|
|
70
|
+
{
|
|
71
|
+
"analytics_storage": "denied",
|
|
72
|
+
**dict.fromkeys(AD_SIGNALS, "denied"),
|
|
73
|
+
"region": list(EUROPE),
|
|
74
|
+
"wait_for_update": WAIT_FOR_UPDATE,
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
return defaults
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_signed_in(request):
|
|
81
|
+
user = getattr(request, "user", None)
|
|
82
|
+
return bool(user and user.is_authenticated)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def tag_config(context, overrides):
|
|
86
|
+
"""The browser script's configuration. "id" is None when the tag doesn't
|
|
87
|
+
load: no measurement ID, a preview, or a signed-in user who isn't tracked."""
|
|
88
|
+
request = context.get("request")
|
|
89
|
+
if request is None:
|
|
90
|
+
return {"id": None}
|
|
91
|
+
|
|
92
|
+
site_settings = get_settings(request)
|
|
93
|
+
measurement_id = get_measurement_id(site_settings)
|
|
94
|
+
if (
|
|
95
|
+
not measurement_id
|
|
96
|
+
or getattr(request, "is_preview", False)
|
|
97
|
+
or (is_signed_in(request) and not site_settings.track_signed_in_users)
|
|
98
|
+
):
|
|
99
|
+
return {"id": None}
|
|
100
|
+
|
|
101
|
+
page = context.get("page")
|
|
102
|
+
params = page_params(page) if isinstance(page, Page) else {}
|
|
103
|
+
params.update(overrides)
|
|
104
|
+
if site_settings.debug_mode:
|
|
105
|
+
params["debug_mode"] = True
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
"id": measurement_id,
|
|
109
|
+
"consent": consent_defaults(site_settings),
|
|
110
|
+
"params": params,
|
|
111
|
+
"ads": site_settings.uses_google_ads,
|
|
112
|
+
"contactLinks": site_settings.track_contact_links,
|
|
113
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django import template
|
|
2
|
+
|
|
3
|
+
from ..tag import tag_config
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
register = template.Library()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register.inclusion_tag("wagtail_google_tag/google_tag.html", takes_context=True)
|
|
10
|
+
def google_tag(context, **params):
|
|
11
|
+
"""Google's tag, with Consent Mode defaults and the page's data, and the
|
|
12
|
+
window.wagtailGoogleTag script API. Keyword arguments add page_view
|
|
13
|
+
parameters, for templates without a Wagtail page:
|
|
14
|
+
{% google_tag content_group="Not found" %}."""
|
|
15
|
+
return {"config": tag_config(context, params)}
|