@guardian/ophan-tracker-js 2.1.1 → 2.1.2-next.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/NPM-dist/adblock-detection.js +20 -0
- package/NPM-dist/attention.js +126 -0
- package/NPM-dist/campaign.js +23 -0
- package/NPM-dist/click-path-capture.js +68 -0
- package/NPM-dist/components.js +168 -0
- package/{assets → NPM-dist}/contribution.js +4 -13
- package/NPM-dist/core.js +230 -0
- package/{assets → NPM-dist}/embed.js +3 -6
- package/{assets → NPM-dist}/fb-instant.js +2 -5
- package/NPM-dist/heatmap.js +34 -0
- package/{assets → NPM-dist}/holidays.js +3 -6
- package/{assets → NPM-dist}/http-status.js +4 -6
- package/NPM-dist/iframe-tracking.js +80 -0
- package/NPM-dist/interactive.js +17 -0
- package/{assets → NPM-dist}/jobs-courses.js +3 -6
- package/{assets → NPM-dist}/jobs.js +3 -6
- package/NPM-dist/manage-my-account.js +29 -0
- package/{assets → NPM-dist}/membership.js +4 -13
- package/NPM-dist/ng.js +46 -0
- package/NPM-dist/perf.js +36 -0
- package/NPM-dist/privatebrowsing.js +66 -0
- package/{assets → NPM-dist}/r2.js +3 -12
- package/{assets → NPM-dist}/smart-news.js +4 -10
- package/NPM-dist/support.js +28 -0
- package/NPM-dist/tests/click-path-capture.test.js +44 -0
- package/NPM-dist/tests/components.test.js +47 -0
- package/NPM-dist/tests/core.test.js +161 -0
- package/NPM-dist/tests/heatmap.test.js +45 -0
- package/NPM-dist/tests/transmit.test.js +118 -0
- package/NPM-dist/transmit.js +126 -0
- package/NPM-dist/types/abtest.js +1 -0
- package/NPM-dist/types/acquisition.js +1 -0
- package/NPM-dist/types/adblock-detection.d.ts +4 -0
- package/NPM-dist/types/attention.d.ts +9 -0
- package/NPM-dist/types/campaign.d.ts +1 -0
- package/NPM-dist/types/click-path-capture.d.ts +14 -0
- package/NPM-dist/types/component-event.js +1 -0
- package/NPM-dist/types/component-type.js +1 -0
- package/NPM-dist/types/components.d.ts +60 -0
- package/NPM-dist/types/consent.js +1 -0
- package/NPM-dist/types/contribution.d.ts +5 -0
- package/NPM-dist/types/core.d.ts +64 -0
- package/NPM-dist/types/embed.d.ts +5 -0
- package/NPM-dist/types/event.js +1 -0
- package/NPM-dist/types/fb-instant.d.ts +4 -0
- package/NPM-dist/types/heatmap.d.ts +5 -0
- package/NPM-dist/types/holidays.d.ts +5 -0
- package/NPM-dist/types/http-status.d.ts +5 -0
- package/NPM-dist/types/iframe-tracking.d.ts +1 -0
- package/NPM-dist/types/inpageclick.js +1 -0
- package/NPM-dist/types/interaction.js +1 -0
- package/NPM-dist/types/interactive.d.ts +1 -0
- package/NPM-dist/types/jobs-courses.d.ts +5 -0
- package/NPM-dist/types/jobs.d.ts +5 -0
- package/NPM-dist/types/link.js +1 -0
- package/NPM-dist/types/manage-my-account.d.ts +7 -0
- package/NPM-dist/types/media.js +1 -0
- package/NPM-dist/types/membership.d.ts +5 -0
- package/NPM-dist/types/ng.d.ts +26 -0
- package/NPM-dist/types/perf.d.ts +2 -0
- package/NPM-dist/types/platform.js +1 -0
- package/NPM-dist/types/printproduct.js +1 -0
- package/NPM-dist/types/privatebrowsing.d.ts +6 -0
- package/NPM-dist/types/product.js +1 -0
- package/NPM-dist/types/r2.d.ts +4 -0
- package/NPM-dist/types/smart-news.d.ts +5 -0
- package/NPM-dist/types/subscription.js +1 -0
- package/NPM-dist/types/support.d.ts +9 -0
- package/NPM-dist/types/tests/click-path-capture.test.d.ts +1 -0
- package/NPM-dist/types/tests/components.test.d.ts +1 -0
- package/NPM-dist/types/tests/core.test.d.ts +1 -0
- package/NPM-dist/types/tests/heatmap.test.d.ts +1 -0
- package/NPM-dist/types/tests/transmit.test.d.ts +1 -0
- package/NPM-dist/types/transmit.d.ts +17 -0
- package/NPM-dist/types/types/abtest.d.ts +28 -0
- package/NPM-dist/types/types/acquisition.d.ts +116 -0
- package/NPM-dist/types/types/component-event.d.ts +86 -0
- package/NPM-dist/types/types/component-type.d.ts +1 -0
- package/NPM-dist/types/types/consent.d.ts +40 -0
- package/NPM-dist/types/types/event.d.ts +106 -0
- package/NPM-dist/types/types/inpageclick.d.ts +11 -0
- package/NPM-dist/types/types/interaction.d.ts +17 -0
- package/NPM-dist/types/types/link.d.ts +14 -0
- package/NPM-dist/types/types/media.d.ts +98 -0
- package/NPM-dist/types/types/platform.d.ts +10 -0
- package/NPM-dist/types/types/printproduct.d.ts +27 -0
- package/NPM-dist/types/types/product.d.ts +53 -0
- package/NPM-dist/types/types/subscription.d.ts +60 -0
- package/NPM-dist/types/types/utmparameters.d.ts +26 -0
- package/NPM-dist/types/utmparameters.js +1 -0
- package/NPM-dist/types/vendor/adBlockDetectionLib.d.ts +4 -0
- package/NPM-dist/types/visibility.d.ts +5 -0
- package/NPM-dist/types/witness.d.ts +5 -0
- package/NPM-dist/vendor/adBlockDetectionLib.js +312 -0
- package/NPM-dist/visibility.js +22 -0
- package/{assets → NPM-dist}/witness.js +3 -6
- package/package.json +18 -8
- package/readme.md +98 -11
- package/assets/adblock-detection.js +0 -19
- package/assets/attention.js +0 -160
- package/assets/campaign.js +0 -25
- package/assets/click-path-capture.js +0 -80
- package/assets/components.js +0 -199
- package/assets/core.js +0 -282
- package/assets/heatmap.js +0 -53
- package/assets/iframe-tracking.js +0 -88
- package/assets/interactive.js +0 -17
- package/assets/manage-my-account.js +0 -38
- package/assets/ng.js +0 -41
- package/assets/perf.js +0 -40
- package/assets/privatebrowsing.js +0 -71
- package/assets/support.js +0 -34
- package/assets/transmit.js +0 -143
- package/assets/vendor/adBlockDetectionLib.js +0 -366
- package/assets/visibility.js +0 -23
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import abd from './vendor/adBlockDetectionLib.js';
|
|
2
|
+
export default {
|
|
3
|
+
run() {
|
|
4
|
+
return new Promise((resolve, reject) => {
|
|
5
|
+
if (abd.init) {
|
|
6
|
+
try {
|
|
7
|
+
abd.init({
|
|
8
|
+
complete: resolve,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
reject(error);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
reject(new Error('Failed to initialise adblock detection'));
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
},
|
|
20
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Generated by CoffeeScript 2.7.0
|
|
2
|
+
import transmit from './transmit';
|
|
3
|
+
import components from './components.js';
|
|
4
|
+
// The upworthy definition of attention minutes, from
|
|
5
|
+
// http://upworthy.github.io/2014/06/implementing-attention-minutes-part-1/ :
|
|
6
|
+
// "We consider the page to have the user’s attention if
|
|
7
|
+
// 1. some activity is happening on the page. For our purposes this means:
|
|
8
|
+
// 1. the page currently has focus
|
|
9
|
+
// 2. and the user has interacted with the page within a certain timeout
|
|
10
|
+
// 2. or a video is playing on the page
|
|
11
|
+
// Attention Minutes, then, are the total number of minutes that a user has spent on
|
|
12
|
+
// a page where the above conditions have held true."
|
|
13
|
+
// our definition of "within a certain timeout" above:
|
|
14
|
+
const ATTENTIONDECAY = 5000;
|
|
15
|
+
// our definition of "the user has interacted with the page"
|
|
16
|
+
const EVENTS = [
|
|
17
|
+
'focus',
|
|
18
|
+
'click',
|
|
19
|
+
'scroll',
|
|
20
|
+
'mousemove',
|
|
21
|
+
'touchstart',
|
|
22
|
+
'touchend',
|
|
23
|
+
'touchcancel',
|
|
24
|
+
'touchleave',
|
|
25
|
+
'touchmove',
|
|
26
|
+
'keyup',
|
|
27
|
+
'keydown',
|
|
28
|
+
];
|
|
29
|
+
const REPORTINGINTERVAL = 10000;
|
|
30
|
+
// total attention time so far on this page
|
|
31
|
+
let totalAttentionMs = 0;
|
|
32
|
+
// the time elapsed since the time origin (https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin)
|
|
33
|
+
// when the most recent period of attention (that is, attention
|
|
34
|
+
// which we haven't yet added to totalAttentionMs) began.
|
|
35
|
+
// null if we don't have attention.
|
|
36
|
+
let unrecordedAttentionStarted = null;
|
|
37
|
+
// the attention time we last reported to ophan
|
|
38
|
+
let reportedTotalAttentionMs = null; // setting null here forces a first report of attention time, even if it's only 0ms
|
|
39
|
+
// the decay timer if we've set one
|
|
40
|
+
let decayTimerId = null;
|
|
41
|
+
// is video currently playing?
|
|
42
|
+
let videoPlaying = false;
|
|
43
|
+
const cancelDecayTimer = () => {
|
|
44
|
+
if (decayTimerId != null) {
|
|
45
|
+
window.clearTimeout(decayTimerId);
|
|
46
|
+
}
|
|
47
|
+
decayTimerId = null;
|
|
48
|
+
};
|
|
49
|
+
const makeActive = () => {
|
|
50
|
+
if (unrecordedAttentionStarted == null) {
|
|
51
|
+
unrecordedAttentionStarted = performance.now();
|
|
52
|
+
}
|
|
53
|
+
components.startMonitoring();
|
|
54
|
+
cancelDecayTimer();
|
|
55
|
+
decayTimerId = window.setTimeout(() => {
|
|
56
|
+
if (!videoPlaying) {
|
|
57
|
+
makeInactive();
|
|
58
|
+
}
|
|
59
|
+
}, ATTENTIONDECAY);
|
|
60
|
+
};
|
|
61
|
+
const incrementTotalAttentionTimeByUnrecordedAmount = () => {
|
|
62
|
+
if (unrecordedAttentionStarted != null) {
|
|
63
|
+
const now = performance.now();
|
|
64
|
+
const unrecordedMs = Math.min(now - unrecordedAttentionStarted, REPORTINGINTERVAL);
|
|
65
|
+
totalAttentionMs += unrecordedMs;
|
|
66
|
+
unrecordedAttentionStarted = now;
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
const makeInactive = () => {
|
|
70
|
+
cancelDecayTimer();
|
|
71
|
+
components.stopMonitoring();
|
|
72
|
+
incrementTotalAttentionTimeByUnrecordedAmount();
|
|
73
|
+
unrecordedAttentionStarted = null;
|
|
74
|
+
};
|
|
75
|
+
const reporter = () => {
|
|
76
|
+
incrementTotalAttentionTimeByUnrecordedAmount();
|
|
77
|
+
if (totalAttentionMs !== reportedTotalAttentionMs) {
|
|
78
|
+
const report = {
|
|
79
|
+
attentionMs: Math.round(totalAttentionMs),
|
|
80
|
+
};
|
|
81
|
+
const componentAttentionTimes = components.getAttentionTimes();
|
|
82
|
+
if (Object.keys(componentAttentionTimes).length) {
|
|
83
|
+
report.componentAttentionMs = componentAttentionTimes;
|
|
84
|
+
}
|
|
85
|
+
transmit.sendMore(report);
|
|
86
|
+
reportedTotalAttentionMs = totalAttentionMs;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
const initComponent = (name, el, visibilityThreshold = 0.5) => {
|
|
90
|
+
components.registerComponent(name, el, visibilityThreshold, REPORTINGINTERVAL);
|
|
91
|
+
};
|
|
92
|
+
const initAttention = (visibility) => {
|
|
93
|
+
EVENTS.forEach((event) => {
|
|
94
|
+
window.addEventListener(event, makeActive);
|
|
95
|
+
});
|
|
96
|
+
document.addEventListener(visibility.changeEvent, () => {
|
|
97
|
+
if (visibility.state() === 'visible') {
|
|
98
|
+
makeActive();
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
makeInactive();
|
|
102
|
+
}
|
|
103
|
+
}, false);
|
|
104
|
+
document.addEventListener('videoPlaying', () => {
|
|
105
|
+
videoPlaying = true;
|
|
106
|
+
makeActive();
|
|
107
|
+
});
|
|
108
|
+
document.addEventListener('videoEnded', () => {
|
|
109
|
+
videoPlaying = false;
|
|
110
|
+
makeInactive();
|
|
111
|
+
});
|
|
112
|
+
document.addEventListener('videoPause', () => {
|
|
113
|
+
videoPlaying = false;
|
|
114
|
+
makeInactive();
|
|
115
|
+
});
|
|
116
|
+
window.setTimeout(reporter, 100);
|
|
117
|
+
window.setInterval(reporter, REPORTINGINTERVAL);
|
|
118
|
+
};
|
|
119
|
+
const setEventEmitter = (emitter) => {
|
|
120
|
+
components.setEventEmitter(emitter);
|
|
121
|
+
};
|
|
122
|
+
export default {
|
|
123
|
+
init: initAttention,
|
|
124
|
+
initComponent: initComponent,
|
|
125
|
+
setEventEmitter: setEventEmitter,
|
|
126
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Generated by CoffeeScript 2.7.0
|
|
2
|
+
import transmit from './transmit';
|
|
3
|
+
import perf from './perf.js';
|
|
4
|
+
const start = perf?.timing?.domComplete ?? performance.now();
|
|
5
|
+
// DFP ads
|
|
6
|
+
if (typeof googletag !== 'undefined' && googletag !== null) {
|
|
7
|
+
googletag.cmd.push(() => {
|
|
8
|
+
googletag.pubads().addEventListener('slotRenderEnded', (event) => {
|
|
9
|
+
const slotId = event.slot.getSlotId().getDomId();
|
|
10
|
+
transmit.sendMore({
|
|
11
|
+
ads: [
|
|
12
|
+
{
|
|
13
|
+
slot: slotId,
|
|
14
|
+
campaignId: event.isEmpty ? '__empty__' : event.lineItemId,
|
|
15
|
+
creativeId: event.creativeId,
|
|
16
|
+
timeToRenderEnded: Math.round(performance.now() - start),
|
|
17
|
+
adServer: 'DFP',
|
|
18
|
+
},
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import ophan from './core.js';
|
|
2
|
+
import transmit from './transmit';
|
|
3
|
+
/**
|
|
4
|
+
* Finds the closest ancestor anchor (`<a>`) element of the given element.
|
|
5
|
+
*
|
|
6
|
+
* @param {Element} el - The element to start searching from.
|
|
7
|
+
* @returns {Element|null} The closest ancestor anchor element or null if none found.
|
|
8
|
+
*/
|
|
9
|
+
export const validAncestorAnchorElement = (el) => {
|
|
10
|
+
if (!el || el.nodeName?.toLowerCase() === 'body')
|
|
11
|
+
return null;
|
|
12
|
+
return el.nodeName.toLowerCase() === 'a'
|
|
13
|
+
? el
|
|
14
|
+
: validAncestorAnchorElement(el.parentNode);
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Retrieves the value of the `data-component` attribute from the closest ancestor.
|
|
18
|
+
*
|
|
19
|
+
* @param {Element} el - The element to start searching from.
|
|
20
|
+
* @returns {string|null} The `data-component` attribute value or null if not found.
|
|
21
|
+
*/
|
|
22
|
+
export const getContainingComponent = (el) => {
|
|
23
|
+
if (!el || el.nodeName?.toLowerCase() === 'body')
|
|
24
|
+
return null;
|
|
25
|
+
return (el.getAttribute('data-component') || getContainingComponent(el.parentNode));
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Collects all `data-link-name` attribute values from the ancestor elements.
|
|
29
|
+
*
|
|
30
|
+
* @param {Element} el - The element to start searching from.
|
|
31
|
+
* @param {string[]} dataLinkNames - Initial array of data link names (optional).
|
|
32
|
+
* @returns {string[]} Array of `data-link-name` values.
|
|
33
|
+
*/
|
|
34
|
+
const getDataLinkNames = (el, dataLinkNames = []) => {
|
|
35
|
+
if (!el || el === document.body || el === document)
|
|
36
|
+
return dataLinkNames;
|
|
37
|
+
const dataLinkName = el.getAttribute('data-link-name');
|
|
38
|
+
if (dataLinkName)
|
|
39
|
+
dataLinkNames.push(dataLinkName);
|
|
40
|
+
return getDataLinkNames(el.parentNode, dataLinkNames);
|
|
41
|
+
};
|
|
42
|
+
if (typeof document.addEventListener === 'function') {
|
|
43
|
+
document.addEventListener('click', function (e) {
|
|
44
|
+
let anchorTarget, info;
|
|
45
|
+
anchorTarget = validAncestorAnchorElement(e.target);
|
|
46
|
+
info = {
|
|
47
|
+
from: [location.protocol, '//', location.host, location.pathname].join(''),
|
|
48
|
+
to: anchorTarget ? anchorTarget.href : void 0,
|
|
49
|
+
referringComponent: getContainingComponent(e.target),
|
|
50
|
+
referringDataLinkNames: getDataLinkNames(e.target),
|
|
51
|
+
refPlatform: ophan.servingPlatform(),
|
|
52
|
+
refViewId: ophan.viewId,
|
|
53
|
+
};
|
|
54
|
+
if (info.referringDataLinkNames != null) {
|
|
55
|
+
transmit.sendMore({
|
|
56
|
+
clickComponent: info.referringComponent,
|
|
57
|
+
clickLinkNames: info.referringDataLinkNames,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (anchorTarget != null) {
|
|
61
|
+
// then we may be about to navigate away from this page, so store to send on next request
|
|
62
|
+
return ophan.storeDataToSendOnNextEvent(info);
|
|
63
|
+
}
|
|
64
|
+
}, false);
|
|
65
|
+
}
|
|
66
|
+
export default {
|
|
67
|
+
getDataLinkNames: getDataLinkNames,
|
|
68
|
+
};
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @type {Component[]}
|
|
3
|
+
*/
|
|
4
|
+
export const components = [];
|
|
5
|
+
/**
|
|
6
|
+
* theguardian.com uses a custom scroll event for performance reasons.
|
|
7
|
+
* We pass in the event emitter so that we can listen to this custom scroll event.
|
|
8
|
+
* If not on theguardian.com, we default to the native scroll event
|
|
9
|
+
* @type {Object|null}
|
|
10
|
+
*/
|
|
11
|
+
let eventEmitter = null;
|
|
12
|
+
// Tracks attention time of elements on the page with data-component attributes.
|
|
13
|
+
// This should always be less than or equal to the page attention time.
|
|
14
|
+
// If the page does not have attention, no component has attention.
|
|
15
|
+
// If the page does have attention, a given component *may* also have
|
|
16
|
+
// attention, if it is within the viewport and not hidden.
|
|
17
|
+
class Component {
|
|
18
|
+
/**
|
|
19
|
+
* Component constructor.
|
|
20
|
+
* @param {string} name - The name of the component.
|
|
21
|
+
* @param {HTMLElement} element - The DOM element of the component.
|
|
22
|
+
* @param {number} visibilityThreshold1 -
|
|
23
|
+
* visibilityThreshold represents the fraction of each component that must
|
|
24
|
+
* be in the viewport before it is considered visible.
|
|
25
|
+
* e.g. 0.5 means that half the height or width must be in the viewport
|
|
26
|
+
* 1 means that the entire element must be in the viewport
|
|
27
|
+
* @param {number} reportingInterval1 - ??
|
|
28
|
+
*/
|
|
29
|
+
constructor(name, element, visibilityThreshold1, reportingInterval1) {
|
|
30
|
+
this.name = name;
|
|
31
|
+
this.element = element;
|
|
32
|
+
this.visibilityThreshold = visibilityThreshold1;
|
|
33
|
+
this.reportingInterval = reportingInterval1;
|
|
34
|
+
this.visible = false;
|
|
35
|
+
// total attention time so far for this element
|
|
36
|
+
this.totalAttentionMs = 0;
|
|
37
|
+
// the time elapsed since the time origin (https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp#the_time_origin)
|
|
38
|
+
// when the most recent period of attention (that is, attention
|
|
39
|
+
// which we haven't yet added to totalAttentionMs) began.
|
|
40
|
+
// null if we don't have attention.
|
|
41
|
+
this.unrecordedAttentionStarted = null;
|
|
42
|
+
// the attention time when getAttentionTime() was last called
|
|
43
|
+
this.reportedTotalAttentionMs = 0;
|
|
44
|
+
this.usingEmitter = false;
|
|
45
|
+
this.visCheck = this.checkVisibility.bind(this);
|
|
46
|
+
if (eventEmitter != null) {
|
|
47
|
+
this.usingEmitter = true;
|
|
48
|
+
eventEmitter.on('window:throttledScroll', this.visCheck);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
window.addEventListener('scroll', this.visCheck);
|
|
52
|
+
}
|
|
53
|
+
window.addEventListener('resize', this.visCheck);
|
|
54
|
+
}
|
|
55
|
+
isVisible(threshold = 1) {
|
|
56
|
+
if (!this.element.offsetParent) {
|
|
57
|
+
// exclude hidden elements
|
|
58
|
+
// will also exclude position: fixed elements but we don't care about these
|
|
59
|
+
// (because their attention time would always be the same as the whole page)
|
|
60
|
+
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
const box = this.element.getBoundingClientRect();
|
|
64
|
+
const width = box.width;
|
|
65
|
+
const height = box.height;
|
|
66
|
+
const windowHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
67
|
+
const windowWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
68
|
+
return (box.left >= -(width * (1 - threshold)) &&
|
|
69
|
+
box.top >= -(height * (1 - threshold)) &&
|
|
70
|
+
box.right <= windowWidth + width * (1 - threshold) &&
|
|
71
|
+
box.bottom <= windowHeight + height * (1 - threshold));
|
|
72
|
+
}
|
|
73
|
+
visibilityHasChanged() {
|
|
74
|
+
const wasVisible = this.visible;
|
|
75
|
+
this.visible = this.isVisible(this.visibilityThreshold);
|
|
76
|
+
return wasVisible !== this.visible;
|
|
77
|
+
}
|
|
78
|
+
rebindToEventEmitter() {
|
|
79
|
+
if (!this.usingEmitter && eventEmitter != null) {
|
|
80
|
+
window.removeEventListener('scroll', this.visCheck);
|
|
81
|
+
this.usingEmitter = true;
|
|
82
|
+
return eventEmitter.on('window:throttledScroll', this.visCheck);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
checkVisibility() {
|
|
86
|
+
this.rebindToEventEmitter();
|
|
87
|
+
if (this.visibilityHasChanged()) {
|
|
88
|
+
return this.visible ? this.makeActive() : this.makeInactive();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
makeActive() {
|
|
92
|
+
// mark when the latest period of attention began,
|
|
93
|
+
// if we're not already in an attention period
|
|
94
|
+
return this.unrecordedAttentionStarted != null
|
|
95
|
+
? this.unrecordedAttentionStarted
|
|
96
|
+
: (this.unrecordedAttentionStarted = performance.now());
|
|
97
|
+
}
|
|
98
|
+
makeInactive() {
|
|
99
|
+
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
100
|
+
return (this.unrecordedAttentionStarted = null);
|
|
101
|
+
}
|
|
102
|
+
hadAttentionSinceLastGet() {
|
|
103
|
+
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
104
|
+
return this.totalAttentionMs !== this.reportedTotalAttentionMs;
|
|
105
|
+
}
|
|
106
|
+
getAttentionTime() {
|
|
107
|
+
this.incrementTotalAttentionTimeByUnrecordedAmount();
|
|
108
|
+
this.reportedTotalAttentionMs = this.totalAttentionMs;
|
|
109
|
+
return this.totalAttentionMs;
|
|
110
|
+
}
|
|
111
|
+
incrementTotalAttentionTimeByUnrecordedAmount() {
|
|
112
|
+
if (this.unrecordedAttentionStarted != null) {
|
|
113
|
+
const now = performance.now();
|
|
114
|
+
// the Math.min here is to deal with occasions where we don't get an event indicating that the user
|
|
115
|
+
// has become inactive - this can happen e.g. due to users suspending their phone. Never send
|
|
116
|
+
// crazy big values!
|
|
117
|
+
const unrecordedMs = Math.min(now - this.unrecordedAttentionStarted, this.reportingInterval);
|
|
118
|
+
this.totalAttentionMs += unrecordedMs;
|
|
119
|
+
return (this.unrecordedAttentionStarted = now);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Begins monitoring the components on the page for attention time.
|
|
125
|
+
* @returns {Array} Array of results from makeActive() for each visible component.
|
|
126
|
+
*/
|
|
127
|
+
const startMonitoring = () => {
|
|
128
|
+
return components
|
|
129
|
+
.filter((component) => component.isVisible())
|
|
130
|
+
.map((component) => component.makeActive());
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Stops monitoring the components on the page for attention time.
|
|
134
|
+
* @returns {Array} Array of results from makeInactive() for each component.
|
|
135
|
+
*/
|
|
136
|
+
const stopMonitoring = () => {
|
|
137
|
+
return components.map((component) => component.makeInactive());
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Retrieves the attention times for all components.
|
|
141
|
+
* @returns {Object}
|
|
142
|
+
*/
|
|
143
|
+
const getAttentionTimes = () => {
|
|
144
|
+
const obj = {};
|
|
145
|
+
for (const component of components) {
|
|
146
|
+
if (component.hadAttentionSinceLastGet()) {
|
|
147
|
+
// if there are duplicate component names, only the last one survives
|
|
148
|
+
obj[component.name] = Math.round(component.getAttentionTime());
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return obj;
|
|
152
|
+
};
|
|
153
|
+
export default {
|
|
154
|
+
/**
|
|
155
|
+
* @param {Object} emitter - The event emitter to set.
|
|
156
|
+
* @returns {Object}
|
|
157
|
+
*/
|
|
158
|
+
setEventEmitter: function (emitter) {
|
|
159
|
+
return (eventEmitter = emitter);
|
|
160
|
+
},
|
|
161
|
+
startMonitoring,
|
|
162
|
+
stopMonitoring,
|
|
163
|
+
getAttentionTimes,
|
|
164
|
+
/** @type {(...args: ConstructorParameters<typeof Component>) => void} */
|
|
165
|
+
registerComponent: function (name, el, visibilityThreshold, reportingInterval) {
|
|
166
|
+
return components.push(new Component(name, el, visibilityThreshold, reportingInterval));
|
|
167
|
+
},
|
|
168
|
+
};
|
|
@@ -1,25 +1,16 @@
|
|
|
1
1
|
// Generated by CoffeeScript 2.7.0
|
|
2
2
|
import core from './core.js';
|
|
3
|
-
|
|
4
|
-
import transmit from './transmit.js';
|
|
5
|
-
|
|
3
|
+
import transmit from './transmit';
|
|
6
4
|
import attention from './attention.js';
|
|
7
|
-
|
|
8
5
|
import visibility from './visibility.js';
|
|
9
|
-
|
|
10
6
|
import './click-path-capture.js';
|
|
11
|
-
|
|
12
7
|
import './perf.js';
|
|
13
|
-
|
|
14
8
|
import './campaign.js';
|
|
15
|
-
|
|
16
9
|
core.init('contribution');
|
|
17
|
-
|
|
18
10
|
if (window.addEventListener != null) {
|
|
19
|
-
|
|
11
|
+
attention.init(visibility);
|
|
20
12
|
}
|
|
21
|
-
|
|
22
13
|
export default {
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
record: transmit.sendMore,
|
|
15
|
+
viewId: transmit.viewId,
|
|
25
16
|
};
|
package/NPM-dist/core.js
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
let init, sendInitialEvent, storeDataToSendOnNextEvent, hasProp = {}.hasOwnProperty;
|
|
2
|
+
import transmit from './transmit';
|
|
3
|
+
import visibility from './visibility.js';
|
|
4
|
+
import adblockDetection from './adblock-detection.js';
|
|
5
|
+
const VERSION = 17;
|
|
6
|
+
let doc = window.document;
|
|
7
|
+
const canStoreEventsInLocalStorage = window.localStorage != null && typeof JSON !== 'undefined' && JSON !== null;
|
|
8
|
+
let platform = null;
|
|
9
|
+
const MaximumComponentNameLength = 50;
|
|
10
|
+
init = function (servingPlatform, httpStatus) {
|
|
11
|
+
platform = servingPlatform;
|
|
12
|
+
if (visibility.state() !== 'prerender') {
|
|
13
|
+
return sendInitialEvent(httpStatus);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
if (visibility.changeEvent) {
|
|
17
|
+
return doc.addEventListener(visibility.changeEvent, function () {
|
|
18
|
+
if (visibility.state() === 'visible') {
|
|
19
|
+
return sendInitialEvent(httpStatus);
|
|
20
|
+
}
|
|
21
|
+
}, false);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Retrieves the value of a specified query parameter from the current page's URL.
|
|
27
|
+
* Using an old-fashioned way of finding a query param rather than using searchParams
|
|
28
|
+
* as we don't control which environments the CDN distribution is being used in and so don't know if it's supported.
|
|
29
|
+
* @param {string} paramName - The name of the query parameter to retrieve.
|
|
30
|
+
* @returns {string | undefined} The value of the query parameter, or undefined if not found.
|
|
31
|
+
*/
|
|
32
|
+
const getQueryParameterValue = (paramName) => {
|
|
33
|
+
const queryString = window.location.search.substring(1);
|
|
34
|
+
const queryParams = queryString.split('&');
|
|
35
|
+
for (const param of queryParams) {
|
|
36
|
+
const [key, value] = param.split('=');
|
|
37
|
+
if (key === paramName) {
|
|
38
|
+
return decodeURIComponent(value);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves the referrer URL from the given referrer or a fallback query parameter.
|
|
44
|
+
* If an authentication flow takes place on page load, the referrer is lost.
|
|
45
|
+
* So, if no referrer, fall back to query param holding the value of the referrer before the auth flow began.
|
|
46
|
+
* See https://github.com/guardian/support-frontend/blob/main/support-frontend/app/controllers/AuthCodeFlowController.scala#L105
|
|
47
|
+
* for an example use. * @param {string} referrer - The document referrer.
|
|
48
|
+
* @param {string} referrer - The document referrer.
|
|
49
|
+
* @returns {string} The referrer URL or the fallback query parameter value.
|
|
50
|
+
*/
|
|
51
|
+
const getReferrer = (referrer) => {
|
|
52
|
+
return referrer || getQueryParameterValue('pre-auth-ref') || '';
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Retrieves the type of navigation that occurred to arrive at the current page, using the Performance API.
|
|
56
|
+
* Returns null if the navigation type is not available.
|
|
57
|
+
* @returns {string | null} The navigation type (e.g., 'navigate', 'reload', 'back_forward'), or null if not available.
|
|
58
|
+
*/
|
|
59
|
+
const getNavigationType = () => {
|
|
60
|
+
if (window.performance?.getEntriesByType) {
|
|
61
|
+
const entries = window.performance.getEntriesByType('navigation');
|
|
62
|
+
if (entries.length > 0) {
|
|
63
|
+
return entries[0].type;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
};
|
|
68
|
+
sendInitialEvent = function (httpStatus, url = location.href, referrer = window.document.referrer) {
|
|
69
|
+
const navigationType = getNavigationType();
|
|
70
|
+
// send initial event
|
|
71
|
+
const event = {
|
|
72
|
+
v: VERSION,
|
|
73
|
+
platform: platform,
|
|
74
|
+
url: url,
|
|
75
|
+
ref: getReferrer(referrer),
|
|
76
|
+
visibilityState: visibility.state(),
|
|
77
|
+
isModernBrowser: typeof guardian !== 'undefined' && guardian !== null
|
|
78
|
+
? guardian.isModernBrowser
|
|
79
|
+
: void 0,
|
|
80
|
+
httpStatus: httpStatus,
|
|
81
|
+
tz: new Date().getTimezoneOffset(),
|
|
82
|
+
};
|
|
83
|
+
if (navigationType != null) {
|
|
84
|
+
// Add navigation type as metadata to the event if it's not null
|
|
85
|
+
event.navigationType = navigationType;
|
|
86
|
+
}
|
|
87
|
+
appendContentType(event);
|
|
88
|
+
appendDataFromLocalStorageToEvent(event);
|
|
89
|
+
transmit.sendInitial(event);
|
|
90
|
+
return adblockDetection.run().then(function (adBlockerEnabled) {
|
|
91
|
+
// `adUnitWasHidden` is the query param used
|
|
92
|
+
// by the previous adblock detection, hence reusing it here.
|
|
93
|
+
return transmit.sendMore({
|
|
94
|
+
adUnitWasHidden: adBlockerEnabled,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const onLoadCaptures = [];
|
|
99
|
+
/**
|
|
100
|
+
* Registers a data capture function to be called when the document is fully loaded.
|
|
101
|
+
* The result of this function is then sent to the tracker backend service.
|
|
102
|
+
* If the document is already loaded, this is done immediately.
|
|
103
|
+
*
|
|
104
|
+
* @param {() => Record<string, unknown>} dataCapture - A function that returns an object containing performance metrics such as page load time, network latency, etc.
|
|
105
|
+
* Refer to the `perf.js` file where this function is used for more info.
|
|
106
|
+
*/
|
|
107
|
+
const onLoadCapture = (dataCapture) => {
|
|
108
|
+
if (document.readyState === 'complete') {
|
|
109
|
+
const msg = { ...dataCapture() };
|
|
110
|
+
transmit.sendMore(msg);
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
onLoadCaptures.push(dataCapture);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
/**
|
|
117
|
+
* Processes all registered data capture functions, merges their outputs and sends it to the tracker backend service.
|
|
118
|
+
* If on a 'next-gen' platform, additional component data is appended to the event via the `appendComponentDataToEvent` function.
|
|
119
|
+
*/
|
|
120
|
+
const processCaptures = () => {
|
|
121
|
+
const msg = {};
|
|
122
|
+
for (const capture of onLoadCaptures) {
|
|
123
|
+
const captureData = capture();
|
|
124
|
+
for (const key in captureData) {
|
|
125
|
+
if (captureData.hasOwnProperty(key)) {
|
|
126
|
+
msg[key] = captureData[key];
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (platform === 'next-gen') {
|
|
131
|
+
appendComponentDataToEvent(msg);
|
|
132
|
+
}
|
|
133
|
+
transmit.sendMore(msg);
|
|
134
|
+
};
|
|
135
|
+
window.addEventListener?.('load', processCaptures, false);
|
|
136
|
+
/**
|
|
137
|
+
* Appends data from local storage to a given event object.
|
|
138
|
+
* Specifically targets data stored under the 'ophan_follow' key in localStorage.
|
|
139
|
+
* After appending the data to the event, it removes the data from localStorage.
|
|
140
|
+
* @param {Record<string, unknown>} event - The event object to which the local storage data will be appended.
|
|
141
|
+
*/
|
|
142
|
+
const appendDataFromLocalStorageToEvent = (event) => {
|
|
143
|
+
if (canStoreEventsInLocalStorage) {
|
|
144
|
+
const localStorageItems = JSON.parse(window.localStorage.getItem('ophan_follow'));
|
|
145
|
+
if (localStorageItems != null) {
|
|
146
|
+
for (const key in localStorageItems) {
|
|
147
|
+
if (localStorageItems.hasOwnProperty(key)) {
|
|
148
|
+
event[key] = localStorageItems[key];
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
window.localStorage.removeItem('ophan_follow');
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
/**
|
|
156
|
+
* Trims component names to a maximum length to ensure they are URL safe.
|
|
157
|
+
* If a component name exceeds the maximum length, it is truncated and appended with an ellipsis.
|
|
158
|
+
* @param {string[]} componentNames - An array of component names.
|
|
159
|
+
* @returns {string[]} An array of trimmed component names.
|
|
160
|
+
*/
|
|
161
|
+
const trimComponentNamesToUrlSafeLength = (componentNames) => {
|
|
162
|
+
return componentNames.map((componentName) => componentName.length > MaximumComponentNameLength
|
|
163
|
+
? componentName.substring(0, MaximumComponentNameLength).concat('…')
|
|
164
|
+
: componentName);
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
167
|
+
* Appends data about rendered components to an event object.
|
|
168
|
+
* Extracts component names from elements with a 'data-component' attribute, trims them for URL safety, ensures uniqueness, and adds them to the event.
|
|
169
|
+
* @param {Object} event - The event object to which component data will be appended.
|
|
170
|
+
*/
|
|
171
|
+
const appendComponentDataToEvent = (event) => {
|
|
172
|
+
const componentElements = document.querySelectorAll('[data-component]');
|
|
173
|
+
const componentNames = Array.prototype.slice.call(componentElements).map((el) => el.getAttribute('data-component'));
|
|
174
|
+
if (componentNames.length > 0) {
|
|
175
|
+
const trimmedComponentNames = trimComponentNamesToUrlSafeLength(componentNames);
|
|
176
|
+
// uniquify
|
|
177
|
+
const uniqueComponents = new Set(trimmedComponentNames);
|
|
178
|
+
event.renderedComponents = [...uniqueComponents];
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
storeDataToSendOnNextEvent = function (newData) {
|
|
182
|
+
let existingData, key, value;
|
|
183
|
+
if (canStoreEventsInLocalStorage) {
|
|
184
|
+
existingData =
|
|
185
|
+
JSON.parse(window.localStorage.getItem('ophan_follow')) || {};
|
|
186
|
+
for (key in newData) {
|
|
187
|
+
if (!hasProp.call(newData, key))
|
|
188
|
+
continue;
|
|
189
|
+
value = newData[key];
|
|
190
|
+
existingData[key] = value;
|
|
191
|
+
}
|
|
192
|
+
return window.localStorage.setItem('ophan_follow', JSON.stringify(existingData));
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
/**
|
|
196
|
+
* Determines the 'content type' based on the platform and guardian object.
|
|
197
|
+
* @param {Record<string, unknown>} event - The event object that will be updated with contentType.
|
|
198
|
+
* @param {string} platform - The current platform ('next-gen', 'embed' or other).
|
|
199
|
+
* @param {Record<string, unknown>} guardian - The guardian object.
|
|
200
|
+
*/
|
|
201
|
+
const appendContentType = (event, platform, guardian) => {
|
|
202
|
+
const contentType = platform === 'next-gen' || platform === 'embed'
|
|
203
|
+
? guardian?.config?.page?.contentType
|
|
204
|
+
: guardian?.page?.contentTypes;
|
|
205
|
+
if (contentType) {
|
|
206
|
+
event.contentType = contentType.toLowerCase();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
export default {
|
|
210
|
+
init: init,
|
|
211
|
+
storeDataToSendOnNextEvent: storeDataToSendOnNextEvent,
|
|
212
|
+
onLoadCapture: onLoadCapture,
|
|
213
|
+
servingPlatform: function () {
|
|
214
|
+
return platform;
|
|
215
|
+
},
|
|
216
|
+
viewId: transmit.viewId,
|
|
217
|
+
sendInitialEvent: sendInitialEvent,
|
|
218
|
+
};
|
|
219
|
+
/**
|
|
220
|
+
* Exports for testing purposes.
|
|
221
|
+
*/
|
|
222
|
+
export const _testExports = {
|
|
223
|
+
appendComponentDataToEvent,
|
|
224
|
+
trimComponentNamesToUrlSafeLength,
|
|
225
|
+
getQueryParameterValue,
|
|
226
|
+
onLoadCaptures,
|
|
227
|
+
onLoadCapture,
|
|
228
|
+
processCaptures,
|
|
229
|
+
appendContentType,
|
|
230
|
+
};
|
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
// Generated by CoffeeScript 2.7.0
|
|
2
2
|
import core from './core.js';
|
|
3
|
-
|
|
4
|
-
import transmit from './transmit.js';
|
|
5
|
-
|
|
3
|
+
import transmit from './transmit';
|
|
6
4
|
core.init('embed');
|
|
7
|
-
|
|
8
5
|
export default {
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
record: transmit.sendMore,
|
|
7
|
+
viewId: transmit.viewId,
|
|
11
8
|
};
|