Track article content consumption based on DOM events
The ArticleTracker connects to the article container DOM element and tracks its contents using specific ArticleElement classes and then provides unified interface ready for reporting.
npm install @optimics/article-tracker
The basic usage could look like this, if you need to only track article paragraphs.
import { ArticleTracker, ArticleParagraph } from '@optimics/article-tracker'
const articleElement = document.querySelector('#article')
const articleTracker = new ArticleTracker(articleElement, {
contentTypes: [
ArticleParagraph
]
})
articleElement.on('elementsDisplayed', () => {
// Connect to your tracking scripts
console.log(articleTracker.getMetrics())
})
articleElement.on('elementsConsumed', () => {
// Connect to your tracking scripts
console.log(articleTracker.getMetrics())
})
articleElement.track()
The ArticleParagraph
is a basic Element Type, that only tracks <p>
tags
content. It estimates slowest and fastest reader time based on the text
content size.
The default selector is just 'p'
.
import { ArticleParagraph } from '@optimics/article-tracker'
import { ArticleTracker, ArticleParagraph, ArticleElement } from '@optimics/article-tracker'
class ArticleVideo extends ArticleElement {
static selector = '.my-video-player'
type = 'videos'
getMetadata() {
/* In this example, we assume, that video length is stored inside the DOM
* element like this. It is up to you, to write your bindings
* <div class="my-video-player" data-player-length="90" /> */
return this.el.dataset.player
}
estimateFastestTime() {
/* In this example, we assume, that the element can be considered consumed
* after 75 % of the video length has been played */
return this.getMetadata().length * 0.75
}
estimateSlowestTime() {
return this.getMetadata().length
}
}
const articleElement = document.querySelector('#article')
const articleTracker = new ArticleTracker(articleElement, {
contentTypes: [
ArticleParagraph,
ArticleVideo,
]
})
The ArticleTracker
metrics are split by the Content Types, but also includes
aggregated values. When you call articleTracker.getMetrics
, you might get
something like this:
{
"achieved": 0.05,
"consumed": false,
"overtime": 0,
"timeTotal": 25,
"content": {
"paragraph": {
"achived": 0.05,
"consumed": false,
"consumableElements": 4,
"consumedElements": 0,
"detected": 5,
"displayed": 1,
"timeTotal": 25,
"estimates": {
"fastest": 36.4,
"slowest": 64.5
}
}
},
"estimates": {
"fastest": 36.4,
"slowest": 64.5
}
}
The values in the root of the metrics object is aggregated from the individual elements.
Percentage of how much of the context have been consumed. Rounded to two decimals.
Available on: ArticleMetrics
, ContentTypeMetrics
Each Content Type has specific measurement logic, that determines if it is
appropriate to mark it consumed. Let's take ArticleParagraph
for example.
The ArticleParagraph
estimates the fastest and the slowest consumption time
based on the amount of words in the paragraph. We measure time the paragraph
spends on screen and when it reached the fastest consumption time, we mark it
consumed
.
The entire Article is consumed
, when all of its elements have been
consumed
.
Available on: ArticleMetrics
, ContentTypeMetrics
How many elements of this type can be consumed? Empty elements of a type are not counted into the consumption metrics. For example empty paragraph would be ignored.
Available on: ContentTypeMetrics
How many elements of this type have been consumed?
Available on: ContentTypeMetrics
How many elements of this type have been detected?
Available on: ContentTypeMetrics
Calculated estimate of the fastest time required to consume the entire content in seconds.
Calculated estimate of the slowest time required to consume the entire content in seconds.
Extra time user spent consuming this content. This is a natural number multiplier of the slowest consumer time from TimeEstimates. User, that reached twice the time of slowest consumer will have value 1. Three times the slowest consumer will be 2, and so on. Useful for filtering out unuseful analytics metrics.
Available on: ArticleMetrics
How much time did user spend on the element or article in seconds.
Available on: ArticleMetrics
, ContentTypeMetrics
Article Tracker automagically triggers events as result of user interaction.
The EventHandler is a function, that returns void and always receives props
objects with at least articleTracker
instance. All of the events are
debounced, so they do not trigger too often.
To subscribe to ArticleTracker event, use the following subscription pattern:
function handler() {
doSomething()
}
articleTracker.consumptionAchievement.subscribe(handler, options)
Supported events are described below. The options
are optional and may be ommited.
once
- when true, the event handler will be triggered only once and then unboundconditions
- filter the callbacks
Reports progress of consumption achievement. Currently triggered together with
elementsConsumed
event but the API might change in the future to provide
details with higher resolution.
articleTracker.on('consumptionAchievement', ({ articleTracker, achieved }) => {
console.log(achieved)
})
Fired when the Article Tracker changes state between "no elements being
consumed" and "some elements being consumed". The callback gets passed boolean
property consuming
. When consuming
is true
, it means, that some elements
in the article are being consumed by the user.
- Debounced event
- Squashed event on
consuming
articleTracker.on('consumptionStarted', ({ consuming }) => {
console.log(consuming)
})
Fired when the article tracker changes state from "no elements being consumed" to "some elements being consumed".
articleTracker.on('consumptionStarted', ({ articleTracker }) => {
console.log(articleTracker.getMetrics())
})
Fired when the article tracker changes state from "some elements being consumed" to "no elements being consumed".
articleTracker.on('consumptionStopped', ({ articleTracker }) => {
console.log(articleTracker.getMetrics())
})
This is triggered whenever an element, that has not been displayed on page yet,
has been diplayed in the page viewport. The event receives targets
prop,
containing reference to all article elements, that have been displayed since
last call.
articleTracker.on('elementsDisplayed', ({ articleTracker, targets }) => {
console.log(articleTracker.getMetrics())
console.log(targets)
})
This is triggered whenever an element, that has not been consumed yet, has met
conditions to be marked as consumed. This event receives targets
prop,
containing reference to all article elements, that have been consumed since
last call.
articleTracker.on('elementsConsumed', ({ articleTracker, targets }) => {
console.log(articleTracker.getMetrics())
console.log(targets)
})
articleTracker.on('overtime', ({ articleTracker }) => {
console.log(articleTracker.getMetrics())
})
The test suite is a mixture of two environments. JSDOM is used for interface testing and Puppeteer is used for the integration testing to leverage a real Chrome environment. Please note, that Puppeteer tests are not always stable, take long time to run and may sometimes fail due to timeouts and other various instabilities. Rerunning tests on sporadic failures is recommended.
The test suite is included in the repository main test suite.
You can use these variables to develop and debug Puppeteer tests.
Setting TEST_GRAPHIC=true
will disable test headless mode and will open
actual Chrome window, so you can see the literal behaviour with your own eyes.
This is useful when you want to understand what is happening inside the
browser.
The browser window will partially take control of your operating system window focus, so you might not be able to fully use your computer during the test. Make sure you select specific test you want to run.
Setting TEST_DEBUG=true
will keep the browser window open for 60 minutes
after the test has ended. It implies TEST_GRAPHIC
. It is useful, when you
want to inspect the Browser Developer Tools
The test suite starts subprocesses, that occasionally fail to exit on unexpected test exit. Namingly, this is webpack-dev-server and Chrome browser. This will consume resources of your operating system, eventually rendering it unusable. If you experience issues, like heavy swapping and low memory, terminate them manually.
This will terminate all processes named chrome and node. Do not use it unless you are sure what you are doing.
killall chrome
killall node