@financial-times/n-tracking
Advanced tools
Comparing version 1.1.0-beta.1 to 1.1.0-beta.2
@@ -166,2 +166,6 @@ import oTracking from '@financial-times/o-tracking'; | ||
const isContextComplete = (context) => { | ||
return requiredMetrics.every((metric) => typeof context[metric] === 'number'); | ||
}; | ||
const realUserMonitoringForPerformance = () => { | ||
@@ -177,13 +181,10 @@ | ||
const navigation = performance.getEntriesByType('navigation')[0]; | ||
const { type, domInteractive, domComplete } = navigation; | ||
// Proceed only if the page load event is a "navigate". | ||
// @see: https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type | ||
if (type !== 'navigate') return; | ||
if (navigation.type !== 'navigate') return; | ||
const context = { | ||
action: 'performance', | ||
category: 'page', | ||
domInteractive: Math.round(domInteractive), | ||
domComplete: Math.round(domComplete), | ||
domInteractive: Math.round(navigation.domInteractive), | ||
domComplete: Math.round(navigation.domComplete), | ||
}; | ||
@@ -199,2 +200,3 @@ | ||
let hasAlreadyBroadcast = false; | ||
options.analyticsTracker = (({ metricName, duration, data }) => { | ||
@@ -208,11 +210,16 @@ if (hasAlreadyBroadcast) return; | ||
} | ||
else if (metricName === 'navigationTiming') { | ||
if (metricName === 'navigationTiming') { | ||
context.timeToFirstByte = Math.round(data.timeToFirstByte); | ||
} | ||
// Broadcast only if all the metrics are present | ||
const contextContainsAllRequiredMetrics = requiredMetrics.every(metric => !isNaN(context[metric])); | ||
if (contextContainsAllRequiredMetrics) { | ||
console.log({performanceMetrics:context}); // eslint-disable-line no-console | ||
broadcast('oTracking.event', context); | ||
if (isContextComplete(context)) { | ||
console.log({ performanceMetrics: context }); // eslint-disable-line no-console | ||
broadcast('oTracking.event', { | ||
action: 'performance', | ||
category: 'page', | ||
context | ||
}); | ||
hasAlreadyBroadcast = true; | ||
@@ -225,2 +232,238 @@ } | ||
// Create markers at each of these percentage points | ||
const DEPTH_MARKERS = [25, 50, 75, 100]; | ||
const defaultOptions = { | ||
onScroll: () => {}, | ||
target: 'body', | ||
debug: false | ||
}; | ||
class ScrollDepth { | ||
constructor (options) { | ||
this.options = { ...defaultOptions, ...options }; | ||
this.init(); | ||
} | ||
init () { | ||
const target = document.querySelector(this.options.target); | ||
if (target && 'IntersectionObserver' in window) { | ||
this.observer = new IntersectionObserver(this.handleIntersection.bind(this)); | ||
target.style.position = 'relative'; | ||
DEPTH_MARKERS.forEach((percentage) => { | ||
const marker = document.createElement('div'); | ||
marker.className = 'n-tracking-scroll-depth-marker'; | ||
marker.style.position = 'absolute'; | ||
marker.style.top = `${percentage}%`; | ||
marker.style.bottom = '0'; | ||
marker.style.width = '100%'; | ||
marker.style.zIndex = '-1'; | ||
marker.setAttribute('data-scroll-depth', percentage); | ||
target.appendChild(marker); | ||
this.observer.observe(marker); | ||
}); | ||
} | ||
} | ||
handleIntersection (changes) { | ||
changes.forEach((change) => { | ||
if (change.isIntersecting || change.intersectionRatio > 0) { | ||
const marker = change.target; | ||
const scrollDepth = marker.getAttribute('data-scroll-depth'); | ||
this.options.onScroll(scrollDepth); | ||
if (this.options.debug) { | ||
console.log('ScrollDepth', { marker: scrollDepth }); // eslint-disable-line no-console | ||
} | ||
if (marker.parentNode) { | ||
marker.parentNode.removeChild(marker); | ||
} | ||
this.observer.unobserve(marker); | ||
} | ||
}); | ||
}; | ||
} | ||
// Automatically stop the attention timer after this time | ||
const ATTENTION_INTERVAL = 15000; | ||
// These events re/start the attention timer | ||
const ATTENTION_EVENTS = [ | ||
'load', | ||
'click', | ||
'focus', | ||
'scroll', | ||
'mousemove', | ||
'touchstart', | ||
'touchend', | ||
'touchcancel', | ||
'touchleave' | ||
]; | ||
// These events pause the attention timer | ||
const ATTENTION_LOST_EVENTS = ['blur']; | ||
// These events will trigger the exit callback | ||
const PAGE_EXIT_EVENTS = ['beforeunload', 'unload', 'pagehide']; | ||
const defaultOptions$1 = { | ||
onExit: () => {}, | ||
debug: false | ||
}; | ||
class AttentionTime { | ||
constructor (options) { | ||
this.options = { ...defaultOptions$1, ...options }; | ||
this.totalAttentionTime = 0; | ||
this.hasExited = false; | ||
this.init(); | ||
} | ||
init () { | ||
ATTENTION_EVENTS.forEach((event) => { | ||
window.addEventListener(event, this.startAttention.bind(this)); | ||
}); | ||
ATTENTION_LOST_EVENTS.forEach((event) => { | ||
window.addEventListener(event, this.endAttention.bind(this)); | ||
}); | ||
PAGE_EXIT_EVENTS.forEach((event) => { | ||
window.addEventListener(event, this.handleExit.bind(this)); | ||
}); | ||
document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this)); | ||
const videoPlayers = document.querySelectorAll('video'); | ||
videoPlayers.forEach((element) => { | ||
element.addEventListener('playing', this.startConstantAttention.bind(this)); | ||
element.addEventListener('pause', this.endConstantAttention.bind(this)); | ||
element.addEventListener('ended', this.endConstantAttention.bind(this)); | ||
}); | ||
} | ||
startAttention (event) { | ||
clearTimeout(this.attentionTimeout); | ||
if (!this.startAttentionTime) { | ||
this.startAttentionTime = Date.now(); | ||
} | ||
this.attentionTimeout = setTimeout( | ||
this.endAttention.bind(this, { type: 'timeout' }), | ||
ATTENTION_INTERVAL | ||
); | ||
if (this.options.debug) { | ||
console.log('AttentionTime start', { event: event.type }); // eslint-disable-line no-console | ||
} | ||
} | ||
endAttention (event) { | ||
if (this.startAttentionTime) { | ||
clearTimeout(this.attentionTimeout); | ||
this.totalAttentionTime = this.getAttentionTime(); | ||
this.startAttentionTime = null; | ||
} | ||
if (this.options.debug) { | ||
console.log('AttentionTime end', { event: event.type, time: this.totalAttentionTime }); // eslint-disable-line no-console | ||
} | ||
} | ||
startConstantAttention () { | ||
this.constantAttentionInterval = setInterval( | ||
this.startAttention.bind(this), | ||
ATTENTION_INTERVAL | ||
); | ||
} | ||
endConstantAttention (event) { | ||
this.endAttention(event); | ||
clearInterval(this.constantAttentionInterval); | ||
} | ||
getAttentionTime () { | ||
let currentAttentionTime = 0; | ||
if (this.startAttentionTime) { | ||
currentAttentionTime = Math.round( | ||
(Date.now() - this.startAttentionTime) / 1000 | ||
); | ||
} | ||
return this.totalAttentionTime + currentAttentionTime; | ||
} | ||
handleVisibilityChange (event) { | ||
if (document.visibilityState === 'hidden') { | ||
this.endAttention(event); | ||
} else { | ||
this.startAttention(event); | ||
} | ||
} | ||
handleExit (event) { | ||
if (this.hasExited) { | ||
return; | ||
} | ||
this.endAttention(event); | ||
if (this.options.debug) { | ||
console.log('AttentionTime', { event: event.type, time: this.totalAttentionTime }); // eslint-disable-line no-console | ||
} | ||
this.options.onExit(this.totalAttentionTime); | ||
this.hasExited = true; | ||
} | ||
} | ||
// TODO: The tracking event data for the `page:interaction` and `page:scrolldepth` | ||
// events is needlessly different. We should work with the data team to align it. | ||
const pageAttention = (options = {}) => { | ||
const onExit = (attentionTime) => { | ||
broadcast('oTracking.event', { | ||
category: 'page', | ||
action: 'interaction', | ||
context: { | ||
attention: { | ||
total: attentionTime | ||
} | ||
} | ||
}); | ||
}; | ||
const attention = new AttentionTime({ ...options, onExit }); | ||
const onScroll = (scrollDepth) => { | ||
broadcast('oTracking.event', { | ||
category: 'page', | ||
action: 'scrolldepth', | ||
meta: { | ||
percentagesViewed: scrollDepth, | ||
attention: attention.getAttentionTime() | ||
} | ||
}); | ||
}; | ||
new ScrollDepth({ ...options, onScroll }); | ||
}; | ||
// TODO: implement and test trackers copied over from n-ui | ||
@@ -230,3 +473,4 @@ | ||
__proto__: null, | ||
realUserMonitoringForPerformance: realUserMonitoringForPerformance | ||
realUserMonitoringForPerformance: realUserMonitoringForPerformance, | ||
pageAttention: pageAttention | ||
}); | ||
@@ -233,0 +477,0 @@ |
@@ -6,3 +6,3 @@ { | ||
"browser": "dist/browser.js", | ||
"version": "1.1.0-beta.1", | ||
"version": "1.1.0-beta.2", | ||
"license": "MIT", | ||
@@ -9,0 +9,0 @@ "repository": "Financial-Times/n-tracking.git", |
@@ -43,14 +43,2 @@ # @financial-times/n-tracking [![CircleCI](https://circleci.com/gh/Financial-Times/n-tracking/tree/master.svg?style=svg)](https://circleci.com/gh/Financial-Times/n-tracking/tree/master) | ||
**Send real-user-monitoring (RUM) performance metrics** | ||
```js | ||
import * as nTracking from '@financial-times/n-tracking'; | ||
if (flags.get('realUserMonitoringForPerformance')) { | ||
nTracking.trackers.realUserMonitoringForPerformance(); | ||
} | ||
``` | ||
<div><img width="70%" src="https://user-images.githubusercontent.com/224547/71626767-c709c480-2be6-11ea-91a5-506972a3b4d7.png" /></div> | ||
_Above: Real-user-monitoring performance metrics are sent to spoor-api._ | ||
### Server-side integration | ||
@@ -79,5 +67,5 @@ | ||
### `trackers.customTrackerName()` | ||
### `trackers.{tracker}()` | ||
TODO: custom tracking events to be used across FT.com | ||
There are several custom tracking features provided by this library. See the [docs folder](./docs) for more information about these. | ||
@@ -84,0 +72,0 @@ |
// TODO: implement and test trackers copied over from n-ui | ||
// https://github.com/Financial-Times/n-ui/tree/master/components/n-ui/tracking/ft/events | ||
import { realUserMonitoringForPerformance } from './real-user-monitoring-for-performance'; | ||
export { realUserMonitoringForPerformance }; | ||
export * from './realUserMonitoringForPerformance'; | ||
export * from './pageAttention'; |
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
604924
34
15200
94