-
Notifications
You must be signed in to change notification settings - Fork 226
Plugin: Element Tracking #1400
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
jethron
wants to merge
30
commits into
snowplow:master
Choose a base branch
from
jethron:plugin-element-tracking
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+3,217
−7
Open
Plugin: Element Tracking #1400
Changes from all commits
Commits
Show all changes
30 commits
Select commit
Hold shift + click to select a range
d71c772
Initial element tracking plugin
jethron 3e640c9
Further work on element tracking plugin
jethron 7c6a1ff
ShadowRoot support
jethron 651df52
Update demo page/docs
jethron 97298dc
Updates & docs
jethron 4d031e7
Unit tests
jethron 164aab4
More doc updates
jethron 9700424
Fix demo page
jethron f805747
Reset observers after previous shutdown
jethron 3624dbc
Shuffle circular imports
jethron 9e3d8f2
Make individual observers optional
jethron 449eb83
Validate shadow selectors
jethron c0a1f8a
Small tidy
jethron 5c4bbfb
More tests
jethron bc78452
Tests + fix for multiple trackers
jethron 0b2273e
Update state handling
jethron 503ddf0
Add stats tracking
jethron 185688b
Schema changes
jethron dccb862
Add page ping support to demo page
jethron 3778091
Integrate Element Tracking plugin with repo
jethron 94bc0fc
Test for element stats
jethron b21920f
Migrate demo page to README
jethron 376cf6b
Run rush change
jethron 4925f59
Fix package.json version
jethron ec283b8
Make tests v4 compatible
jethron 0238cd8
Fix subtree create/destroy detection
jethron 9983080
Update element_statistics for schema feedback
jethron 132cfff
element: keep track of originating page view ID
jethron 68b231b
Use previously known size for obscure/destroy events
jethron e595ef5
Fix createdTs handling
jethron File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
10 changes: 10 additions & 0 deletions
10
...s/@snowplow/browser-plugin-element-tracking/plugin-element-tracking_2024-12-11-04-22.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"changes": [ | ||
{ | ||
"packageName": "@snowplow/browser-plugin-element-tracking", | ||
"comment": "Create element tracking plugin", | ||
"type": "none" | ||
} | ||
], | ||
"packageName": "@snowplow/browser-plugin-element-tracking" | ||
} |
10 changes: 10 additions & 0 deletions
10
common/changes/@snowplow/javascript-tracker/plugin-element-tracking_2024-12-11-04-22.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"changes": [ | ||
{ | ||
"packageName": "@snowplow/javascript-tracker", | ||
"comment": "Add support for element tracking plugin", | ||
"type": "none" | ||
} | ||
], | ||
"packageName": "@snowplow/javascript-tracker" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
// DO NOT MODIFY THIS FILE MANUALLY BUT DO COMMIT IT. It is generated and used by Rush. | ||
{ | ||
"pnpmShrinkwrapHash": "483ab7c144cc1201cfba40405c05ebf190586e79", | ||
"pnpmShrinkwrapHash": "1bbfee8474092dd7a04f769c89f237e4c74bfbb5", | ||
"preferredVersionsHash": "bf21a9e8fbc5a3846fb05b4fa0859e0917b2202f" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
BSD 3-Clause License | ||
|
||
Copyright (c) 2023 Snowplow Analytics Ltd, 2010 Anthon Pang | ||
All rights reserved. | ||
|
||
Redistribution and use in source and binary forms, with or without | ||
modification, are permitted provided that the following conditions are met: | ||
|
||
1. Redistributions of source code must retain the above copyright notice, this | ||
list of conditions and the following disclaimer. | ||
|
||
2. Redistributions in binary form must reproduce the above copyright notice, | ||
this list of conditions and the following disclaimer in the documentation | ||
and/or other materials provided with the distribution. | ||
|
||
3. Neither the name of the copyright holder nor the names of its | ||
contributors may be used to endorse or promote products derived from | ||
this software without specific prior written permission. | ||
|
||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | ||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | ||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | ||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | ||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | ||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | ||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | ||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | ||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | ||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,195 @@ | ||
# Snowplow Element Tracking Plugin | ||
|
||
[![npm version][npm-image]][npm-url] | ||
[![License][license-image]](LICENSE) | ||
|
||
Browser Plugin to be used with `@snowplow/browser-tracker`. | ||
|
||
This plugin is allows tracking the addition/removal and visibility of page elements. | ||
|
||
## Maintainer quick start | ||
|
||
Part of the Snowplow JavaScript Tracker monorepo. | ||
Build with [Node.js](https://nodejs.org/en/) (18 - 20) and [Rush](https://rushjs.io/). | ||
|
||
### Setup repository | ||
|
||
```bash | ||
npm install -g @microsoft/rush | ||
git clone https://github.com/snowplow/snowplow-javascript-tracker.git | ||
rush update | ||
``` | ||
|
||
## Package Installation | ||
|
||
With npm: | ||
|
||
```bash | ||
npm install @snowplow/browser-plugin-element-tracking | ||
``` | ||
|
||
## Usage | ||
|
||
Initialize your tracker with the SnowplowElementTrackingPlugin and then call `startElementTracking`: | ||
|
||
```js | ||
import { newTracker } from '@snowplow/browser-tracker'; | ||
import { SnowplowElementTrackingPlugin, startElementTracking } from '@snowplow/browser-plugin-element-tracking'; | ||
|
||
newTracker('sp1', '{{collector_url}}', { | ||
appId: 'my-app-id', | ||
plugins: [ SnowplowElementTrackingPlugin() ], | ||
}); | ||
|
||
startElementTracking({ | ||
elements: [ | ||
{selector: '.newsletter-signup'} | ||
] | ||
}); | ||
``` | ||
|
||
### Configuration | ||
|
||
Configuration occurs primarily via the `elements` setting passed to `startElementTracking`. | ||
|
||
You can pass a single configuration or an array of multiple configurations. | ||
|
||
Each configuration requires a `selector`, with a CSS selector describing the elements the configuration applies to. | ||
All other configuration is optional. | ||
|
||
You can label each configuration with the `name` property (if not specified, the `selector` is used as the `name`). | ||
The `name` is used in the event payloads and matches the `element_name` of any entities specific to the target element(s). | ||
|
||
The settings control triggering events for: | ||
|
||
- `expose_element`: When a selected element enters the viewport, becoming visible | ||
- `obscure_element`: When a selected element exists the viewport, no longer being visible | ||
- `create_element`: When a selected element is created or exists in the document | ||
- `destroy_element`: When a selected element is removed from or no longer found in the document | ||
|
||
Each of these events can be enabled, disabled, or configured more specifically. | ||
By default, only `expose_element` is enabled. | ||
|
||
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. | ||
|
||
The plugin manages the following custom entities: | ||
|
||
- `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. | ||
- `component_parents`: For the element generating the event, provides a list of components (defined by the `component` setting) that are ancestors of that element. | ||
- `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. | ||
- `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. | ||
|
||
A detailed example configuration follows: | ||
|
||
```javascript | ||
snowplow('startElementTracking', { | ||
elements: [ | ||
// can be a single or array of many configurations; additive, can be called multiple times to add more configurations, but doesn't dedupe | ||
{ | ||
selector: '.oncreate', // required: selector for element, relative to document scope by default | ||
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 | ||
create: true, // track an event when the element is added to the DOM (or when plugin loads if already on page) (default: false) | ||
destroy: true, // track an event when the element is removed from the DOM (or when plugin loads if already on page) (default: false) | ||
expose: true, // track an event when the element intersects with the viewport (default: true) | ||
obscure: true, // track an event when the element scrolls out of the viewport (default: false) | ||
details: [ | ||
// details can be extracted from the element and included in the entity | ||
function (element) { | ||
return { example: 'from a function' }; | ||
}, // use a function that returns an object | ||
{ 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 | ||
{ attributes: ['class'] }, // attributes: get the static/default attributes originally defined on the element when created | ||
{ properties: ['className'] }, // properties: get the dynamic/current attributes defined on the element | ||
{ dataset: ['example'] }, // dataset: extract values from dataset attributes | ||
{ 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 | ||
{ selector: true }, // selector: attach the matching CSS selector as an attribute; useful if you're using logical names but want to differentiate | ||
{ 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 | ||
], | ||
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 | ||
}, | ||
{ 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 | ||
{ | ||
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 | ||
name: 'mutated div', | ||
create: true, | ||
destroy: true, | ||
expose: false, | ||
obscure: false, | ||
}, | ||
{ | ||
selector: '.perpage.toggled', | ||
name: 'perpage mutation', | ||
create: { when: 'pageview' }, // for each type of event you can specify frequency caps for when the event will fire: never, always, once, element, pageview | ||
destroy: { when: 'pageview' }, | ||
/* | ||
the frequency options are "per": | ||
- per never will never track the event, effectively disabling the configuration | ||
- per always will track an event every time it is eligible (e.g. every time on screen when scrolled past) | ||
- 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 | ||
- per element is like once, but for each individually matching element instance | ||
- 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 | ||
*/ | ||
expose: false, // `false` is equivalent to `when: never`, and `true` is `when: always` | ||
obscure: false, | ||
}, | ||
{ | ||
name: 'recommendations', | ||
selector: '.recommendations', | ||
expose: { | ||
// expose has more options than the other events: | ||
minTimeMillis: 5000, // cumulative time in milliseconds that each matching element should be visible for before considered exposed | ||
minPercentage: 0, // the minimum percentage of the element's area that should be visible before considering exposed; range 0.0 - 1.0 | ||
minSize: 0, // the minimum size the element should be before being considered exposed; this can be used to ignore elements with 0 size | ||
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 | ||
}, | ||
obscure: true, | ||
component: true, | ||
details: { child_text: ['h2'] }, | ||
contents: [ | ||
// content information can be extracted | ||
{ | ||
name: 'recommendation-item', // contents can be named too | ||
selector: 'li', // selectors are relative to the parent element | ||
details: { content: { item_name: /.+/ } }, // content item details can be captured too | ||
contents: { name: 'recommendation_image', selector: 'img', details: { attributes: ['alt'] } }, // you can descend contents like a tree | ||
}, | ||
], | ||
}, | ||
{ | ||
name: 'shadow', | ||
selector: 'button.shadow', | ||
shadowSelector: 'shadow-host', // elements within custom components/shadow hosts require their hosts' selectors to be specified | ||
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 | ||
}, | ||
], | ||
}); | ||
|
||
snowplow('getComponentListGenerator', function (componentGenerator, componentGeneratorWithDetail) { | ||
// access a context generator aware of the startElementTracking "components" configuration | ||
// this will attach the component_parents entity to events generated by these plugins that show the component hierarchy | ||
snowplow('enableLinkClickTracking', { context: [componentGenerator] }); | ||
snowplow('enableFormTracking', { context: [componentGenerator] }); | ||
|
||
// componentGeneratorWithDetail will also populate element_detail entities for each component, but may not be directly compatible with the above APIs | ||
}); | ||
|
||
snowplow('endElementTracking', {elements: ['names']}); // stop tracking all configurations with given `name`s | ||
snowplow('endElementTracking', {elementIds: ['id']}); // to be more specific, each configuration can also have an ID to remove explicitly | ||
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 | ||
snowplow('endElementTracking'); // stop tracking all elements and remove listeners | ||
``` | ||
|
||
|
||
## Copyright and license | ||
|
||
Licensed and distributed under the [BSD 3-Clause License](LICENSE) ([An OSI Approved License][osi]). | ||
|
||
Copyright (c) 2024 Snowplow Analytics Ltd. | ||
|
||
All rights reserved. | ||
|
||
[npm-url]: https://www.npmjs.com/package/@snowplow/browser-plugin-element-tracking | ||
[npm-image]: https://img.shields.io/npm/v/@snowplow/browser-plugin-element-tracking | ||
[docs]: https://docs.snowplowanalytics.com/docs/collecting-data/collecting-from-own-applications/javascript-tracker/ | ||
[osi]: https://opensource.org/licenses/BSD-3-Clause | ||
[license-image]: https://img.shields.io/npm/l/@snowplow/browser-plugin-element-tracking |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.