Skip to content

Commit ae1f36c

Browse files
jethronigneel64
authored andcommitted
Plugin: Element Tracking (#1400)
* Adds element tracking plugin
1 parent a083809 commit ae1f36c

31 files changed

+3217
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@snowplow/browser-plugin-element-tracking",
5+
"comment": "Create element tracking plugin",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@snowplow/browser-plugin-element-tracking"
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@snowplow/javascript-tracker",
5+
"comment": "Add support for element tracking plugin",
6+
"type": "none"
7+
}
8+
],
9+
"packageName": "@snowplow/javascript-tracker"
10+
}

common/config/rush/browser-approved-packages.json

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@
6262
"name": "@snowplow/browser-plugin-ecommerce",
6363
"allowedCategories": [ "trackers" ]
6464
},
65+
{
66+
"name": "@snowplow/browser-plugin-element-tracking",
67+
"allowedCategories": [ "trackers" ]
68+
},
6569
{
6670
"name": "@snowplow/browser-plugin-enhanced-consent",
6771
"allowedCategories": [ "trackers" ]

common/config/rush/pnpm-lock.yaml

+79-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/config/rush/repo-state.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush.
22
{
3-
"pnpmShrinkwrapHash": "483ab7c144cc1201cfba40405c05ebf190586e79",
3+
"pnpmShrinkwrapHash": "1bbfee8474092dd7a04f769c89f237e4c74bfbb5",
44
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f"
55
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2023 Snowplow Analytics Ltd, 2010 Anthon Pang
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# Snowplow Element Tracking Plugin
2+
3+
[![npm version][npm-image]][npm-url]
4+
[![License][license-image]](LICENSE)
5+
6+
Browser Plugin to be used with `@snowplow/browser-tracker`.
7+
8+
This plugin allows tracking the addition/removal and visibility of page elements.
9+
10+
## Maintainer quick start
11+
12+
Part of the Snowplow JavaScript Tracker monorepo.
13+
Build with [Node.js](https://nodejs.org/en/) (18 - 20) and [Rush](https://rushjs.io/).
14+
15+
### Setup repository
16+
17+
```bash
18+
npm install -g @microsoft/rush
19+
git clone https://github.com/snowplow/snowplow-javascript-tracker.git
20+
rush update
21+
```
22+
23+
## Package Installation
24+
25+
With npm:
26+
27+
```bash
28+
npm install @snowplow/browser-plugin-element-tracking
29+
```
30+
31+
## Usage
32+
33+
Initialize your tracker with the SnowplowElementTrackingPlugin and then call `startElementTracking`:
34+
35+
```js
36+
import { newTracker } from '@snowplow/browser-tracker';
37+
import { SnowplowElementTrackingPlugin, startElementTracking } from '@snowplow/browser-plugin-element-tracking';
38+
39+
newTracker('sp1', '{{collector_url}}', {
40+
appId: 'my-app-id',
41+
plugins: [ SnowplowElementTrackingPlugin() ],
42+
});
43+
44+
startElementTracking({
45+
elements: [
46+
{selector: '.newsletter-signup'}
47+
]
48+
});
49+
```
50+
51+
### Configuration
52+
53+
Configuration occurs primarily via the `elements` setting passed to `startElementTracking`.
54+
55+
You can pass a single configuration or an array of multiple configurations.
56+
57+
Each configuration requires a `selector`, with a CSS selector describing the elements the configuration applies to.
58+
All other configuration is optional.
59+
60+
You can label each configuration with the `name` property (if not specified, the `selector` is used as the `name`).
61+
The `name` is used in the event payloads and matches the `element_name` of any entities specific to the target element(s).
62+
63+
The settings control triggering events for:
64+
65+
- `expose_element`: When a selected element enters the viewport, becoming visible
66+
- `obscure_element`: When a selected element exists the viewport, no longer being visible
67+
- `create_element`: When a selected element is created or exists in the document
68+
- `destroy_element`: When a selected element is removed from or no longer found in the document
69+
70+
Each of these events can be enabled, disabled, or configured more specifically.
71+
By default, only `expose_element` is enabled.
72+
73+
Rather than trigger events, configurations can also define the selected elements as "components", which can be listed as a `component_parents` entity for other events; or can have their current state attached to other events (such as page pings) via `element_statistics` entities.
74+
75+
The plugin manages the following custom entities:
76+
77+
- `element`: This is shared between all the above events. It contains the `element_name` from the matching configuration, and data about the element that generated the event. This includes the element's dimensions, position (relative to the viewport and document), how many elements matched it's selector (and the index of the element in question, if you selector matches multiple elements). It will also contain custom attributes you can extract from the element via the `details` configuration.
78+
- `component_parents`: For the element generating the event, provides a list of components (defined by the `component` setting) that are ancestors of that element.
79+
- `element_content`: You can also attach details about child elements of the element that matches your selector. E.g. you can select a recommendations widget, and then extract details about the individual recommendations within it.
80+
- `element_statistics`: This entity can be attached to other events and provides a snapshot of what this plugin has observed at that point; it includes the current/smallest/largest dimensions so far, how long the element has existed since it was first observed, its current/maximum scroll depth, its total time in view, and how many times it has been visible in the viewport.
81+
82+
A detailed example configuration follows:
83+
84+
```javascript
85+
snowplow('startElementTracking', {
86+
elements: [
87+
// can be a single or array of many configurations; additive, can be called multiple times to add more configurations, but doesn't dedupe
88+
{
89+
selector: '.oncreate', // required: selector for element, relative to document scope by default
90+
name: 'created div', // logical name: can be shared across multiple configs; defaults to `selector` if not specified; this is used in event payloads and as a key to reference entities
91+
create: true, // track an event when the element is added to the DOM (or when plugin loads if already on page) (default: false)
92+
destroy: true, // track an event when the element is removed from the DOM (or when plugin loads if already on page) (default: false)
93+
expose: true, // track an event when the element intersects with the viewport (default: true)
94+
obscure: true, // track an event when the element scrolls out of the viewport (default: false)
95+
details: [
96+
// details can be extracted from the element and included in the entity
97+
function (element) {
98+
return { example: 'from a function' };
99+
}, // use a function that returns an object
100+
{ attributes: ['class'], selector: true }, // or declarative options; either as a single object or array elements if you want config re-use; this is less flexible but will be useful to Google Tag Manager where functions may not be able to reference DOM elements
101+
{ attributes: ['class'] }, // attributes: get the static/default attributes originally defined on the element when created
102+
{ properties: ['className'] }, // properties: get the dynamic/current attributes defined on the element
103+
{ dataset: ['example'] }, // dataset: extract values from dataset attributes
104+
{ child_text: { heading: 'h2' } }, // child_text: for each given name:selector pair, extract the textContent of the first child matching selector, if it has text content use that value with the given name; if there's no matching children it will try shadow children
105+
{ selector: true }, // selector: attach the matching CSS selector as an attribute; useful if you're using logical names but want to differentiate
106+
{ content: { textType: /text (\S+)/ } }, //content (map of regex patterns to match text against, first capture group used if detected); if no innerText, will try shadow innerText
107+
],
108+
includeStats: ['page_ping'], // you can include a list of event names here; statistics about elements matching this configuration will be attached as entities to those events; event names don't have to be generated by this plugin so can include built-in events like page_pings or custom events
109+
},
110+
{ selector: 'nav', expose: false, component: true }, // `expose` is true by default so may need disabling; `component` means the name/selector is attached to the component_parents entity list for other events triggered on descendants
111+
{
112+
selector: 'div.toggled', // elements that exist but don't yet match the selector will count as created/destroyed if they later are changed to match it
113+
name: 'mutated div',
114+
create: true,
115+
destroy: true,
116+
expose: false,
117+
obscure: false,
118+
},
119+
{
120+
selector: '.perpage.toggled',
121+
name: 'perpage mutation',
122+
create: { when: 'pageview' }, // for each type of event you can specify frequency caps for when the event will fire: never, always, once, element, pageview
123+
destroy: { when: 'pageview' },
124+
/*
125+
the frequency options are "per":
126+
- per never will never track the event, effectively disabling the configuration
127+
- per always will track an event every time it is eligible (e.g. every time on screen when scrolled past)
128+
- per once will only track the event a single time for each configuration for the duration of the plugin instance; this reduces volume since only the first matching element will fire the event
129+
- per element is like once, but for each individually matching element instance
130+
- per pageview is like once, but useful for single-page-applications with long-lasting plugin instances where you may want to track the element on each virtual pageview
131+
*/
132+
expose: false, // `false` is equivalent to `when: never`, and `true` is `when: always`
133+
obscure: false,
134+
},
135+
{
136+
name: 'recommendations',
137+
selector: '.recommendations',
138+
expose: {
139+
// expose has more options than the other events:
140+
minTimeMillis: 5000, // cumulative time in milliseconds that each matching element should be visible for before considered exposed
141+
minPercentage: 0, // the minimum percentage of the element's area that should be visible before considering exposed; range 0.0 - 1.0
142+
minSize: 0, // the minimum size the element should be before being considered exposed; this can be used to ignore elements with 0 size
143+
boundaryPixels: 0, // arbitrary margins to apply to the element when calculating minPercentage; can be a number to apply to all sides, 2-element array to specify vertical and horizontal, or 4-element array to specify margins for each size individually
144+
},
145+
obscure: true,
146+
component: true,
147+
details: { child_text: ['h2'] },
148+
contents: [
149+
// content information can be extracted
150+
{
151+
name: 'recommendation-item', // contents can be named too
152+
selector: 'li', // selectors are relative to the parent element
153+
details: { content: { item_name: /.+/ } }, // content item details can be captured too
154+
contents: { name: 'recommendation_image', selector: 'img', details: { attributes: ['alt'] } }, // you can descend contents like a tree
155+
},
156+
],
157+
},
158+
{
159+
name: 'shadow',
160+
selector: 'button.shadow',
161+
shadowSelector: 'shadow-host', // elements within custom components/shadow hosts require their hosts' selectors to be specified
162+
shadowOnly: true, // if the selector could erroneously catch elements outside your shadow hosts, you can restrict it to only match in shadows; by default it will match elements in and out of shadow hosts if they match the selector
163+
},
164+
],
165+
});
166+
167+
snowplow('getComponentListGenerator', function (componentGenerator, componentGeneratorWithDetail) {
168+
// access a context generator aware of the startElementTracking "components" configuration
169+
// this will attach the component_parents entity to events generated by these plugins that show the component hierarchy
170+
snowplow('enableLinkClickTracking', { context: [componentGenerator] });
171+
snowplow('enableFormTracking', { context: [componentGenerator] });
172+
173+
// componentGeneratorWithDetail will also populate element_detail entities for each component, but may not be directly compatible with the above APIs
174+
});
175+
176+
snowplow('endElementTracking', {elements: ['names']}); // stop tracking all configurations with given `name`s
177+
snowplow('endElementTracking', {elementIds: ['id']}); // to be more specific, each configuration can also have an ID to remove explicitly
178+
snowplow('endElementTracking', {filter: (config) => true}); // decide for yourself if the configuration should be removed; must explicitly return `true` to remove; "truthy" values will not count
179+
snowplow('endElementTracking'); // stop tracking all elements and remove listeners
180+
```
181+
182+
183+
## Copyright and license
184+
185+
Licensed and distributed under the [BSD 3-Clause License](LICENSE) ([An OSI Approved License][osi]).
186+
187+
Copyright (c) 2024 Snowplow Analytics Ltd.
188+
189+
All rights reserved.
190+
191+
[npm-url]: https://www.npmjs.com/package/@snowplow/browser-plugin-element-tracking
192+
[npm-image]: https://img.shields.io/npm/v/@snowplow/browser-plugin-element-tracking
193+
[docs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-tracker/
194+
[osi]: https://opensource.org/licenses/BSD-3-Clause
195+
[license-image]: https://img.shields.io/npm/l/@snowplow/browser-plugin-element-tracking

0 commit comments

Comments
 (0)