@drevops/axel 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +504 -2
- package/build/axel.drupal.gtag.js +423 -0
- package/build/axel.drupal.gtag.min.js +1 -0
- package/build/axel.drupal.gtm.js +415 -0
- package/build/axel.drupal.gtm.min.js +1 -0
- package/build/axel.drupal.js +381 -0
- package/build/axel.drupal.min.js +1 -0
- package/build/axel.esm.js +420 -0
- package/build/axel.gtag.js +418 -0
- package/build/axel.gtag.min.js +1 -0
- package/build/axel.gtm.js +410 -0
- package/build/axel.gtm.min.js +1 -0
- package/build/axel.js +376 -0
- package/build/axel.min.js +1 -0
- package/package.json +67 -5
- package/src/adapters/ga4.js +60 -0
- package/src/adapters/gtag.js +27 -0
- package/src/adapters/gtm.js +13 -0
- package/src/core/core.js +227 -0
- package/src/core/dom.js +153 -0
- package/src/core/event-types/click.js +130 -0
- package/src/core/event-types/field-complete.js +49 -0
- package/src/core/event-types/form-start.js +27 -0
- package/src/core/event-types/form-submit.js +45 -0
- package/src/index.js +8 -0
- package/src/integrations/drupal.js +31 -0
- package/src/integrations/standalone.js +27 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Alex Skrypnyk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,505 @@
|
|
|
1
|
-
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="" rel="noopener">
|
|
3
|
+
<img width="320" src="logo.svg" alt="Axel logo"></a>
|
|
4
|
+
</p>
|
|
2
5
|
|
|
3
|
-
|
|
6
|
+
<h1 align="center">Axel</h1>
|
|
7
|
+
<h3 align="center">Analytics Context Layer</h3>
|
|
8
|
+
|
|
9
|
+
<div align="center">
|
|
10
|
+
|
|
11
|
+
[](https://github.com/drevops/axel/issues)
|
|
12
|
+
[](https://github.com/drevops/axel/pulls)
|
|
13
|
+
[](https://github.com/drevops/axel/actions/workflows/test-nodejs.yml)
|
|
14
|
+
[](https://codecov.io/gh/drevops/axel)
|
|
15
|
+

|
|
16
|
+

|
|
17
|
+

|
|
18
|
+
[](https://scorecard.dev/viewer/?uri=github.com/drevops/axel)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
Axel turns clicks and form interactions into analytics events. Your server renders `data-axel-*` attributes into the page, and the script reads them when a visitor interacts. An adapter sends each event to its destination: Google Tag Manager, the Google tag or a system you plug in.
|
|
25
|
+
|
|
26
|
+
The script holds no rules about your markup. The attributes decide which elements report, under which event name and with which parameters. This README is for developers adding the script to a site and rendering its attributes.
|
|
27
|
+
|
|
28
|
+
## ✨ Features
|
|
29
|
+
|
|
30
|
+
- Tracks clicks on links and buttons inside elements marked with `data-axel-event`.
|
|
31
|
+
- Tracks form starts, field changes and server-confirmed submissions as opt-in event types.
|
|
32
|
+
- Gathers parameters from the clicked element and its ancestors, the closest of each name winning.
|
|
33
|
+
- Sends the title of the group a click sits in, from a marker or the closest heading before it.
|
|
34
|
+
- Runs your own collector functions to add parameters the markup can't hold.
|
|
35
|
+
- Sends events to Google Tag Manager, the Google tag or an adapter you supply.
|
|
36
|
+
- Never reads what a visitor types or chooses in a form field.
|
|
37
|
+
- Ships as 1 dependency-free file per setup, for a plain page or a Drupal site.
|
|
38
|
+
|
|
39
|
+
## 🤔 Why?
|
|
40
|
+
|
|
41
|
+
Google Analytics can tell you that a visitor clicked a link or submitted a form. To act on that, you need to know which link and which form, and the context around them: the card, the section and the component they sat in.
|
|
42
|
+
|
|
43
|
+
For example, a services page shows a grid of cards, and every card ends in a "Learn more" link. A report that lists clicks on "Learn more" can't tell you which card the visitor picked or which section of the page held it.
|
|
44
|
+
|
|
45
|
+
Without Axel, you get that context by tracking each link and form on its own. In Google Tag Manager, you write a trigger and context variables for every component. In code, a developer adds a `dataLayer.push()` call to every link and form in the templates. On a site with many page types and components, that's an enormous amount of work, and every new component adds more.
|
|
46
|
+
|
|
47
|
+
Axel replaces that per-link work with 1 attribute per component. Mark a component with `data-axel-event="card_click"`, and Axel sends a `card_click` event for every link and button inside it. Mark a form with `data-axel-form`, and Axel reports when a visitor starts it and changes a field, never what they typed. It reports the submission when the server's confirmation message appears on the page. Axel gathers the context for each event itself:
|
|
48
|
+
|
|
49
|
+
- The link text and URL, or the form name and field labels
|
|
50
|
+
- The section and card titles, read from the headings inside elements you mark with `data-axel-group` and `data-axel-subgroup`
|
|
51
|
+
- Every `data-axel-param-*` value set on the element or any element that contains it
|
|
52
|
+
|
|
53
|
+
Axel tracks a new page built from tracked components with no new tracking work.
|
|
54
|
+
|
|
55
|
+
The attributes also give your analytics team a tracking framework. The team defines how every event is tracked and reported on: the event names, the parameters and which components send them. Developers add those attributes to the component templates, and Axel reports every page built from them the same way.
|
|
56
|
+
|
|
57
|
+
<details>
|
|
58
|
+
<summary><strong>🔍 Already on Google Analytics or Tag Manager? See what Axel adds, side by side</strong></summary>
|
|
59
|
+
|
|
60
|
+
### Don't existing analytics tools do this already?
|
|
61
|
+
|
|
62
|
+
Nothing gathers this context for Google Analytics out of the box. These tools differ in where the context comes from:
|
|
63
|
+
|
|
64
|
+
| Tool | Where the context comes from |
|
|
65
|
+
|----------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------|
|
|
66
|
+
| Google Analytics 4 Enhanced Measurement | The clicked link's own URL, classes and ID. It sends no `click` event for links to your own domain. |
|
|
67
|
+
| Google Tag Manager | Selectors and Custom JavaScript variables you write in the container for each component, tied to the theme's CSS classes. |
|
|
68
|
+
| Autocapture in Heap and PostHog | The element's text and position in the page, stored for every click. You define events afterward with CSS selectors, and a selector stops matching when the markup changes. |
|
|
69
|
+
| PostHog `data-ph-capture-attribute-*`, Matomo content tracking and the Adobe Experience Manager data layer | Attributes you add to your templates, each format built for its own product. |
|
|
70
|
+
|
|
71
|
+
Axel reads attributes you add to your templates, like the tools in the last row. It sends the events to Google Tag Manager, the Google tag or an adapter you write.
|
|
72
|
+
|
|
73
|
+
You can also render your own attributes and read them in Google Tag Manager. Its Element Attribute variable reads only the clicked element. Context on the card or section around it needs a Custom JavaScript variable or a community template for each parameter. Axel does that lookup for every parameter on every event.
|
|
74
|
+
|
|
75
|
+
### Compared with Google's tracking
|
|
76
|
+
|
|
77
|
+
For example, take a services grid where both links read "Learn more". Each of the 3 approaches in this section tracks this grid:
|
|
78
|
+
|
|
79
|
+
```html
|
|
80
|
+
<section class="services">
|
|
81
|
+
<h2>Our services</h2>
|
|
82
|
+
<div class="card">
|
|
83
|
+
<h3>Repairs</h3>
|
|
84
|
+
<a class="card-link" href="/services/repairs">Learn more</a>
|
|
85
|
+
</div>
|
|
86
|
+
<div class="card">
|
|
87
|
+
<h3>Installations</h3>
|
|
88
|
+
<a class="card-link" href="/services/installations">Learn more</a>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
To tell the 2 links apart in a report, you need the card title and the section heading.
|
|
94
|
+
|
|
95
|
+
#### Enhanced Measurement
|
|
96
|
+
|
|
97
|
+
Enhanced Measurement in Google Analytics 4 sends `click` only for links to another domain, so it reports neither link. For a link to another domain, it sends `link_url`, `link_domain`, `link_classes`, `link_id` and `outbound`.
|
|
98
|
+
|
|
99
|
+
Its `form_submit` goes out when the visitor submits a form, so a submission the server rejects still counts. Axel sends `form_submit` when the server's confirmation message, marked with `data-axel-form-submitted`, reaches the page.
|
|
100
|
+
|
|
101
|
+
#### Google Tag Manager
|
|
102
|
+
|
|
103
|
+
In Google Tag Manager, a click trigger matches the links by their `card-link` class. A Custom JavaScript variable reads the card title from the clicked element:
|
|
104
|
+
|
|
105
|
+
```js
|
|
106
|
+
function () {
|
|
107
|
+
var card = {{Click Element}}.closest('.card');
|
|
108
|
+
var title = card && card.querySelector('h3');
|
|
109
|
+
|
|
110
|
+
return title ? title.textContent.trim() : undefined;
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
A second variable reads the section heading, and a GA4 event tag maps each variable to a parameter. Every component type needs its own trigger and variables.
|
|
115
|
+
|
|
116
|
+
After a redesign renames `.card`, the variable returns `undefined`. The container sits outside the site's code, so the site's tests never run the variable.
|
|
117
|
+
|
|
118
|
+
#### Axel
|
|
119
|
+
|
|
120
|
+
With Axel, the grid's template holds these attributes:
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<section data-axel-event="card_click" data-axel-param-component="services" data-axel-group>
|
|
124
|
+
<h2 data-axel-group-title>Our services</h2>
|
|
125
|
+
<div data-axel-subgroup>
|
|
126
|
+
<h3 data-axel-subgroup-title>Repairs</h3>
|
|
127
|
+
<a href="/services/repairs">Learn more</a>
|
|
128
|
+
</div>
|
|
129
|
+
<div data-axel-subgroup>
|
|
130
|
+
<h3 data-axel-subgroup-title>Installations</h3>
|
|
131
|
+
<a href="/services/installations">Learn more</a>
|
|
132
|
+
</div>
|
|
133
|
+
</section>
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
`data-axel-event` names the event for every link in the section, and `data-axel-param-component` adds the `component` parameter. `data-axel-group` and `data-axel-subgroup` mark the section and each card, and their `-title` attributes mark the headings. [Attributes](#attributes) lists them all.
|
|
137
|
+
|
|
138
|
+
With `axel.gtm.js`, a click on the second link pushes this object to `window.dataLayer`:
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
{
|
|
142
|
+
event: 'card_click',
|
|
143
|
+
link_text: 'Learn more',
|
|
144
|
+
click_type: 'link',
|
|
145
|
+
link_url: 'https://example.com/services/installations',
|
|
146
|
+
group_title: 'Our services',
|
|
147
|
+
subgroup_title: 'Installations',
|
|
148
|
+
component: 'services',
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
With `axel.gtm.js`, Google Tag Manager still needs a custom event trigger and a GA4 event tag that maps each parameter. With `axel.gtag.js`, Axel sends events through the Google tag's `gtag()` function. Either way, each parameter you report on needs a dimension in Google Analytics, as [Google Analytics limits](#google-analytics-limits) describes.
|
|
153
|
+
|
|
154
|
+
Changing the parameters means editing the template and deploying the site. In Google Tag Manager, a change goes live when you publish the container.
|
|
155
|
+
|
|
156
|
+
#### Axel alongside Enhanced Measurement
|
|
157
|
+
|
|
158
|
+
Enhanced Measurement covers events Axel doesn't send, such as scrolls, site search and video engagement. Follow 2 rules when a page loads both:
|
|
159
|
+
|
|
160
|
+
- Turn off **Form interactions** in Enhanced Measurement when Axel tracks forms. Otherwise both send `form_start` and `form_submit` for a form marked with `data-axel-form`.
|
|
161
|
+
- Give Axel events a name other than `click`, such as `card_click`. Enhanced Measurement sends `click` for links to another domain, so a shared name mixes the 2 in reports.
|
|
162
|
+
|
|
163
|
+
</details>
|
|
164
|
+
|
|
165
|
+
## 📦 Installation
|
|
166
|
+
|
|
167
|
+
Every [release](https://github.com/drevops/axel/releases/latest) attaches 1 ready-made bundle per setup. The [`@drevops/axel`](https://www.npmjs.com/package/@drevops/axel) package on npm holds the same bundles in its `build/` folder. Pick the bundle that matches how your site starts scripts and where your events go:
|
|
168
|
+
|
|
169
|
+
| Bundle | Starts | Sends events to |
|
|
170
|
+
|-------------------------|------------------------------------------|--------------------------------------------------|
|
|
171
|
+
| `axel.gtm.js` | When it loads | Google Tag Manager, through `window.dataLayer` |
|
|
172
|
+
| `axel.gtag.js` | When it loads | The Google tag, through `gtag()` |
|
|
173
|
+
| `axel.js` | When it loads | The adapter in `window.axelAdapter` |
|
|
174
|
+
| `axel.drupal.gtm.js` | In a Drupal behavior | Google Tag Manager, through `window.dataLayer` |
|
|
175
|
+
| `axel.drupal.gtag.js` | In a Drupal behavior | The Google tag, through `gtag()` |
|
|
176
|
+
| `axel.drupal.js` | In a Drupal behavior | The adapter in `window.axelAdapter` |
|
|
177
|
+
| `axel.esm.js` | When your code calls `init()` | The adapter you pass to `init()` |
|
|
178
|
+
|
|
179
|
+
Each bundle also comes minified, such as `axel.gtm.min.js`. A bundle holds only its own adapter, so `axel.gtm.min.js` carries no Google tag code.
|
|
180
|
+
|
|
181
|
+
### Plain page
|
|
182
|
+
|
|
183
|
+
Add the bundle to your pages:
|
|
184
|
+
|
|
185
|
+
```html
|
|
186
|
+
<script src="/path/to/axel.gtm.min.js"></script>
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The script starts tracking as soon as it loads. Its listeners sit on `document`, so it can load in `<head>` before the rest of the page. To change the defaults, see [Settings](#settings).
|
|
190
|
+
|
|
191
|
+
To load the bundle from a CDN, such as jsDelivr serving the npm package, add its Subresource Integrity hash. The browser then refuses to run a file that differs from the release:
|
|
192
|
+
|
|
193
|
+
```html
|
|
194
|
+
<script
|
|
195
|
+
src="https://cdn.jsdelivr.net/npm/@drevops/axel@VERSION/build/axel.gtm.min.js"
|
|
196
|
+
integrity="sha384-HASH"
|
|
197
|
+
crossorigin="anonymous"
|
|
198
|
+
></script>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Replace `VERSION` with a release version such as `0.1.1`. Replace `sha384-HASH` with the bundle's hash from that release's notes.
|
|
202
|
+
|
|
203
|
+
### Drupal
|
|
204
|
+
|
|
205
|
+
Define a library that loads a Drupal bundle:
|
|
206
|
+
|
|
207
|
+
```yaml
|
|
208
|
+
axel:
|
|
209
|
+
js:
|
|
210
|
+
js/axel.drupal.gtm.min.js: { minified: true }
|
|
211
|
+
dependencies:
|
|
212
|
+
- core/drupal
|
|
213
|
+
- core/drupalSettings
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The bundle registers the `axel` behavior. The behavior starts tracking when it first attaches and reads its settings from `drupalSettings.axel`. Later attaches, such as after an AJAX request, change nothing.
|
|
217
|
+
|
|
218
|
+
### ES module
|
|
219
|
+
|
|
220
|
+
To bundle the script with your own code, install the package from npm:
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
npm install @drevops/axel
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Import `init()` and an adapter from the package, which resolves to `axel.esm.js`:
|
|
227
|
+
|
|
228
|
+
```js
|
|
229
|
+
import { init, gtm } from '@drevops/axel';
|
|
230
|
+
|
|
231
|
+
init({ adapter: gtm, types: ['click', 'form_start'] });
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The module's `init()` creates 1 shared instance on its first call. Later calls return that instance and ignore their settings, so a second call never doubles your events.
|
|
235
|
+
|
|
236
|
+
## 🚀 Usage
|
|
237
|
+
|
|
238
|
+
To report the clicks inside a component, set `data-axel-event` on the component:
|
|
239
|
+
|
|
240
|
+
```html
|
|
241
|
+
<section
|
|
242
|
+
data-axel-event="component_click"
|
|
243
|
+
data-axel-param-component="promo"
|
|
244
|
+
>
|
|
245
|
+
<a href="/sale">See the deals</a>
|
|
246
|
+
</section>
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
With `axel.gtm.js`, a click on the link pushes this object to `window.dataLayer`:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
{
|
|
253
|
+
event: 'component_click',
|
|
254
|
+
link_text: 'See the deals',
|
|
255
|
+
click_type: 'link',
|
|
256
|
+
link_url: 'https://example.com/sale',
|
|
257
|
+
component: 'promo',
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Attributes
|
|
262
|
+
|
|
263
|
+
The attributes are the public contract of the library and follow semantic versioning: renaming or removing an attribute is a breaking change.
|
|
264
|
+
|
|
265
|
+
The following table lists every attribute the script reads:
|
|
266
|
+
|
|
267
|
+
| Attribute | Set on | Description |
|
|
268
|
+
|-----------------------------------------|-------------------------------|-----------------------------------------------------------------------------------------------------|
|
|
269
|
+
| `data-axel-event="<name>"` | Component or element | Names the click event for every link and button inside it. The one closest to the clicked element wins. |
|
|
270
|
+
| `data-axel-param-<name>="<value>"` | Any element | Adds a parameter to every event from the element and its descendants. Hyphens in `<name>` become underscores. |
|
|
271
|
+
| `data-axel-skip` | Any element | Excludes the element and its descendants from every event type. |
|
|
272
|
+
| `data-axel-form="<name>"` | Form, or an element around it | Marks a form for `form_start` and `field_complete`, with `<name>` as `form_name`. |
|
|
273
|
+
| `data-axel-form-submitted="<name>"` | Confirmation message | Sends `form_submit` for the form named `<name>` when the element is on the page. |
|
|
274
|
+
| `data-axel-group` | Any element | Adds `group_title` to every click inside it. See [Groups](#groups). |
|
|
275
|
+
| `data-axel-group-title` | Any element inside a group | Marks the element whose text is the group title. |
|
|
276
|
+
| `data-axel-group-scope="<selector>"` | Group | Widens the group title lookup to the closest ancestor matching `<selector>`. |
|
|
277
|
+
| `data-axel-subgroup` | Any element | Adds `subgroup_title` to every click inside it. |
|
|
278
|
+
| `data-axel-subgroup-title` | Any element inside a subgroup | Marks the element whose text is the subgroup title. |
|
|
279
|
+
| `data-axel-subgroup-scope="<selector>"` | Subgroup | Widens the subgroup title lookup to the closest ancestor matching `<selector>`. |
|
|
280
|
+
| `data-axel-collector="<name>"` | Any element | Runs the collector named `<name>` for every event from the element and its descendants. See [Collectors](#collectors). |
|
|
281
|
+
|
|
282
|
+
Parameters gather from the element that received the interaction up through its ancestors. When 2 elements set the same parameter, the closer one wins. A container can set `data-axel-param-component="section"` and a card inside it can override it with `data-axel-param-component="card"`.
|
|
283
|
+
|
|
284
|
+
### Event types
|
|
285
|
+
|
|
286
|
+
Each event type is a module you turn on with the `types` setting. Only `click` is on by default.
|
|
287
|
+
|
|
288
|
+
The following table lists every event type and the parameters the script derives for it:
|
|
289
|
+
|
|
290
|
+
| Type | Sent when | Event name | Derived parameters |
|
|
291
|
+
|------------------|------------------------------------------------------------------------------------|-------------------------------|--------------------------------------------------------------|
|
|
292
|
+
| `click` | A link or button inside `data-axel-event` gets a click or a middle click | The `data-axel-event` value | `link_text` and `click_type`, `link_url` for links and submit buttons, and `group_title` and `subgroup_title` inside groups |
|
|
293
|
+
| `form_start` | Focus first enters a marked form, once per form per page | `form_start` | `form_name`, plus `form_id` and `form_destination` when known |
|
|
294
|
+
| `field_complete` | A field inside a marked form fires `change`, every time it does | `field_complete` | The `form_start` parameters, `field_name` when the field has a `name`, and `field_label` |
|
|
295
|
+
| `form_submit` | A `data-axel-form-submitted` element is on the page or is added to it later | `form_submit` | `form_name` |
|
|
296
|
+
|
|
297
|
+
A `data-axel-param-*` attribute overrides a derived parameter of the same name. For example, `data-axel-param-link-text="Custom text"` replaces the text of the link.
|
|
298
|
+
|
|
299
|
+
The script derives each parameter as follows:
|
|
300
|
+
|
|
301
|
+
- `link_text` is the visible text of the link or button with whitespace collapsed. It skips `aria-hidden="true"` content and falls back to `aria-label`, then `title`. For an input button, it's the button's `value`.
|
|
302
|
+
- `click_type` is `link` for a link and `button` for every other element.
|
|
303
|
+
- `link_url` is the absolute address of the link, without the parts [Privacy](#privacy) describes. For a submit button, it's the button's `formaction`, or the `action` of its form. A submit button with neither sends no `link_url`, and neither does an address that isn't a valid URL.
|
|
304
|
+
- `form_id` is the `id` of the marked element. `form_destination` is the absolute `action` address, when the marked element is a `<form>` with an `action` attribute that holds a valid URL.
|
|
305
|
+
- `field_label` is the text of the field's `<label>`, or its `aria-label`. For a radio button or checkbox inside a `<fieldset>`, it's the fieldset's `<legend>`, because the option's own label is the visitor's answer. A radio button outside a fieldset reports only its `aria-label`.
|
|
306
|
+
|
|
307
|
+
Only the server knows that a submission succeeded, so `form_submit` waits for a confirmation it renders. Render `data-axel-form-submitted` into the confirmation message:
|
|
308
|
+
|
|
309
|
+
```html
|
|
310
|
+
<div data-axel-form-submitted="contact">Thank you for your enquiry.</div>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
The script also detects the attribute when it arrives after the page loads, such as in an AJAX confirmation. To report a submission from your own code, call `formSubmitted()`:
|
|
314
|
+
|
|
315
|
+
```js
|
|
316
|
+
window.axel.formSubmitted('contact', { form_step: '2' });
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`formSubmitted()` sends the event whether or not `form_submit` is in `types`. The `form_submit` type only controls the attribute detection.
|
|
320
|
+
|
|
321
|
+
### Groups
|
|
322
|
+
|
|
323
|
+
A group adds the title of the section a click sits in, such as the heading above a list of links. To send it, set `data-axel-group` on the section:
|
|
324
|
+
|
|
325
|
+
```html
|
|
326
|
+
<section
|
|
327
|
+
data-axel-event="component_click"
|
|
328
|
+
data-axel-group
|
|
329
|
+
>
|
|
330
|
+
<h2>Our services</h2>
|
|
331
|
+
<ul>
|
|
332
|
+
<li><a href="/services/repairs">Repairs</a></li>
|
|
333
|
+
</ul>
|
|
334
|
+
</section>
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
A click on the link sends `group_title: 'Our services'`.
|
|
338
|
+
|
|
339
|
+
The script searches the elements that come before the clicked element inside the group. Those are its preceding siblings and their descendants, then the same for each ancestor up to the group. The closest match wins, in this order:
|
|
340
|
+
|
|
341
|
+
1. An element with `data-axel-group-title`.
|
|
342
|
+
2. A heading that matches the `groupTitleSelector` setting, by default `h2`, `h3` or `h4`.
|
|
343
|
+
|
|
344
|
+
A card heading inside the group also matches `groupTitleSelector`, so a click in a card titled by an `<h3>` sends the card's title as `group_title`. To send the section heading, mark it with `data-axel-group-title`.
|
|
345
|
+
|
|
346
|
+
Titles inside `data-axel-skip` never count. A group with no match sends an empty `group_title`, and a click outside every group sends none.
|
|
347
|
+
|
|
348
|
+
The closest group bounds the search, so a nested group takes no title from its parent. When the heading sits outside the markup you render, set `data-axel-group-scope` on the group to a selector for an ancestor that holds the heading. A selector that matches no ancestor keeps the search inside the group.
|
|
349
|
+
|
|
350
|
+
Subgroups work the same way through `data-axel-subgroup`, `data-axel-subgroup-title` and `data-axel-subgroup-scope`, and send `subgroup_title`. A subgroup title comes only from a `data-axel-subgroup-title` marker, so mark it even when it's a heading.
|
|
351
|
+
|
|
352
|
+
### Privacy
|
|
353
|
+
|
|
354
|
+
Field events never carry what a visitor typed or chose: `field_complete` reports the field's name and label only. The script reads no text from inside a `<select>` or `<textarea>`, even when one sits inside a label or a link. Google Analytics forbids personal information in events, and form fields often hold it.
|
|
355
|
+
|
|
356
|
+
URLs can hold personal information too, such as the token in `/reset?token=abc`. So `link_url` and `form_destination` send each URL without its query string. A `mailto:`, `tel:` or `sms:` URL sends only its scheme, such as `tel:`. The fragment stays, so a link to `#contact` still shows which section it opens. To send every URL as it is, set `fullUrls` to `true` after you've checked your URLs for personal information.
|
|
357
|
+
|
|
358
|
+
Parameters you render yourself are sent as they are. Keep personal information out of `data-axel-param-*` values and out of your own adapters and collectors.
|
|
359
|
+
|
|
360
|
+
### Adapters
|
|
361
|
+
|
|
362
|
+
An adapter sends each event to its destination. The library ships 2 adapters for Google products, and both read `window.dataLayer`:
|
|
363
|
+
|
|
364
|
+
| Adapter | Sends each event as | Use it when |
|
|
365
|
+
|---------|------------------------------------------------------------------------------------------|-------------------------------------|
|
|
366
|
+
| `gtm` | `window.dataLayer.push({ ...params, event: name })` | Google Tag Manager routes your events |
|
|
367
|
+
| `gtag` | `gtag('event', name, params)`, queued in `window.dataLayer` until the Google tag loads | The page loads the Google tag directly |
|
|
368
|
+
|
|
369
|
+
Google Tag Manager reads plain objects from `window.dataLayer`. The Google tag reads only what its own `gtag()` function queues there, so pick the adapter that matches the script your page loads.
|
|
370
|
+
|
|
371
|
+
To send events to another system, such as Segment or `adobeDataLayer`, load `axel.js` or `axel.drupal.js` and define your adapter in `window.axelAdapter` before the bundle starts:
|
|
372
|
+
|
|
373
|
+
```html
|
|
374
|
+
<script>
|
|
375
|
+
window.axelAdapter = (name, params) =>
|
|
376
|
+
window.analytics.track(name, params);
|
|
377
|
+
</script>
|
|
378
|
+
<script src="/path/to/axel.min.js"></script>
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
An adapter is a function that receives the event name and parameters, or an object with these methods:
|
|
382
|
+
|
|
383
|
+
- `send(name, params, context)` delivers the event. `context.debug` is true in debug mode, and `context.settings` holds the instance settings.
|
|
384
|
+
- `validate(name, params)` is optional. It returns an array of warning messages, which debug mode prints to the console.
|
|
385
|
+
|
|
386
|
+
On Drupal, define `window.axelAdapter` in a library that loads on the same pages as `axel.drupal.js`. Drupal attaches behaviors after every library has loaded, so the order of the 2 libraries makes no difference.
|
|
387
|
+
|
|
388
|
+
### Collectors
|
|
389
|
+
|
|
390
|
+
A collector is a function that adds parameters when an event happens, such as the position of a result in a list. To report the position of each clicked search result, define a collector in `window.axelCollectors` before the bundle starts:
|
|
391
|
+
|
|
392
|
+
```html
|
|
393
|
+
<script>
|
|
394
|
+
window.axelCollectors = {
|
|
395
|
+
resultPosition(name, params, { element, source }) {
|
|
396
|
+
const item = element.closest('li');
|
|
397
|
+
|
|
398
|
+
return { position: String([...source.children].indexOf(item) + 1) };
|
|
399
|
+
},
|
|
400
|
+
};
|
|
401
|
+
</script>
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Then name the collector with `data-axel-collector` on the list:
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<ol
|
|
408
|
+
data-axel-event="result_click"
|
|
409
|
+
data-axel-collector="resultPosition"
|
|
410
|
+
>
|
|
411
|
+
<li><a href="/storms">Storm safety</a></li>
|
|
412
|
+
<li><a href="/floods">Flood safety</a></li>
|
|
413
|
+
</ol>
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
A click on the second link sends `position: '2'`.
|
|
417
|
+
|
|
418
|
+
A collector receives these arguments:
|
|
419
|
+
|
|
420
|
+
- `name` is the event name.
|
|
421
|
+
- `params` is a copy of the event parameters.
|
|
422
|
+
- `element` is the element the event is about, and `source` is the element with `data-axel-collector`.
|
|
423
|
+
|
|
424
|
+
The collector returns an object, and its entries override every other parameter. The closest `data-axel-collector` wins. Collectors run for every event type, and `formSubmitted()` runs none.
|
|
425
|
+
|
|
426
|
+
An unknown name, a collector that throws or a result that isn't an object logs an error to the console. The event still goes out, without the collector's parameters.
|
|
427
|
+
|
|
428
|
+
With `axel.esm.js`, pass the same object as the `collectors` setting. The bundles add `window.axelCollectors` to that setting, which suits Drupal because `drupalSettings` can't hold functions.
|
|
429
|
+
|
|
430
|
+
### Settings
|
|
431
|
+
|
|
432
|
+
To change the defaults on a plain page, define `window.axelSettings` before the bundle loads:
|
|
433
|
+
|
|
434
|
+
```html
|
|
435
|
+
<script>
|
|
436
|
+
window.axelSettings = {
|
|
437
|
+
types: ['click', 'form_start', 'field_complete', 'form_submit'],
|
|
438
|
+
properties: { site: 'marketing' },
|
|
439
|
+
};
|
|
440
|
+
</script>
|
|
441
|
+
<script src="/path/to/axel.gtm.min.js"></script>
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
On Drupal, put the same keys in `drupalSettings.axel`. With `axel.esm.js`, pass them to `init()`.
|
|
445
|
+
|
|
446
|
+
The following table lists every setting:
|
|
447
|
+
|
|
448
|
+
| Setting | Default | Description |
|
|
449
|
+
|-------------------|------------------------------------------------------------------|-------------------------------------------------------------------------------------|
|
|
450
|
+
| `types` | `['click']` | Event types to track: `click`, `form_start`, `field_complete` and `form_submit`. The list replaces the default, so include `click` to keep tracking clicks. |
|
|
451
|
+
| `adapter` | None | The adapter, for `init()` in `axel.esm.js`. The bundles set it themselves and ignore this setting. |
|
|
452
|
+
| `properties` | `{}` | Static parameters added to every event. Every other parameter overrides them. |
|
|
453
|
+
| `clickSelector` | `'a[href], button, input[type="submit"], input[type="button"], input[type="reset"]'` | Elements whose clicks the `click` type tracks. |
|
|
454
|
+
| `groupTitleSelector` | `'h2, h3, h4'` | Headings that title a group without a `data-axel-group-title` marker. |
|
|
455
|
+
| `collectors` | `{}` | [Collectors](#collectors) by name. |
|
|
456
|
+
| `fullUrls` | `false` | If true, `link_url` and `form_destination` keep the query string and the address of a `mailto:`, `tel:` or `sms:` URL. See [Privacy](#privacy). |
|
|
457
|
+
| `debugHash` | `'#axel-debug'` | URL hash that turns on debug mode. |
|
|
458
|
+
| `debugStorageKey` | `'axelDebug'` | `sessionStorage` key that keeps debug mode on until the tab closes. |
|
|
459
|
+
| `autoInit` | `true` | If true, a plain-page bundle starts tracking when it loads. If false, call `window.axel.init()` yourself. Drupal bundles always start in the behavior. |
|
|
460
|
+
| `dataLayerName` | `'dataLayer'` | Name of the data layer array on `window`. Only the `gtm` and `gtag` adapters read it. |
|
|
461
|
+
|
|
462
|
+
A missing adapter, an unknown event type or a collector that isn't a function throws an error when the instance is created, so a typo fails before any event is lost.
|
|
463
|
+
|
|
464
|
+
Every bundle except `axel.esm.js` exposes the class as `window.Axel` and the shared instance as `window.axel`. Call `destroy()` on an instance to stop tracking, and `init()` to start again with the same settings.
|
|
465
|
+
|
|
466
|
+
### Google Analytics limits
|
|
467
|
+
|
|
468
|
+
Google Analytics 4 applies these limits to every event:
|
|
469
|
+
|
|
470
|
+
- An event name has at most 40 characters.
|
|
471
|
+
- An event has at most 25 parameters.
|
|
472
|
+
- A parameter value has at most 100 characters.
|
|
473
|
+
- A standard property has at most 50 event-scoped custom dimensions, and each parameter you report on needs its own dimension.
|
|
474
|
+
|
|
475
|
+
In debug mode, the `gtm` and `gtag` adapters warn in the console when an event breaks 1 of the first 3 limits.
|
|
476
|
+
|
|
477
|
+
### Debugging
|
|
478
|
+
|
|
479
|
+
To log every event to the console, append `#axel-debug` to the URL. Debug mode stays on in that tab and in tabs opened from it until the tab closes, so it holds on the pages your clicks open. Turn on **Preserve log** in the browser console to keep the output across navigations.
|
|
480
|
+
|
|
481
|
+
In debug mode, the `gtm` and `gtag` adapters add `debug_mode: true` to every event, so Google Analytics lists it in DebugView.
|
|
482
|
+
|
|
483
|
+
## 🔒 Security
|
|
484
|
+
|
|
485
|
+
Axel runs on every page that loads it. Here's what a bundle does on the page:
|
|
486
|
+
|
|
487
|
+
- It reads `data-axel-*` attributes and listens for clicks, focus and field changes on `document`.
|
|
488
|
+
- It watches the page for `data-axel-form-submitted` elements added after load.
|
|
489
|
+
- It hands each event to the adapter. The adapter, such as Google Tag Manager, makes the network requests; the bundle makes none of its own.
|
|
490
|
+
- It stores 1 value: the debug flag in `sessionStorage`. It sets no cookies.
|
|
491
|
+
|
|
492
|
+
`tests/bundles.test.js` fails when a built bundle contains a network, storage or code-evaluation API, such as `fetch()`, `document.cookie` or `eval()`.
|
|
493
|
+
|
|
494
|
+
Axel trusts the page that loads it. `window.axelSettings`, `window.axelAdapter` and `window.axelCollectors` carry the same trust as the page's own scripts, because any script on the page can set them. Load the bundle only on pages whose scripts you control.
|
|
495
|
+
|
|
496
|
+
Anyone can add `#axel-debug` to a URL. Debug mode logs events to that visitor's own console. The `gtm` and `gtag` adapters also add `debug_mode: true` to that visitor's events, so Google Analytics lists them in DebugView.
|
|
497
|
+
|
|
498
|
+
[`SECURITY.md`](SECURITY.md) explains how to report a vulnerability and how to verify a bundle against its release.
|
|
499
|
+
|
|
500
|
+
## 🤝 Contributing
|
|
501
|
+
|
|
502
|
+
See [`CONTRIBUTING.md`](CONTRIBUTING.md) to set up a local environment, run the checks and pull updates from the project template. [`docs/architecture/README.md`](docs/architecture/README.md) explains how the library works inside.
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
_This repository was created using the [Scaffold](https://getscaffold.dev/) project template_
|