@phantomstudios/ft-lib 0.2.0
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/.babelrc +6 -0
- package/.eslintrc +58 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/workflows/pr.yml +31 -0
- package/.github/workflows/release.yml +44 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/CODE_OF_CONDUCT.md +76 -0
- package/CONTRIBUTING.md +48 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/SUPPORT.md +4 -0
- package/jest.config.js +24 -0
- package/lib/FTTracking/index.d.ts +24 -0
- package/lib/FTTracking/index.d.ts.map +1 -0
- package/lib/FTTracking/index.js +69 -0
- package/lib/FTTracking/index.js.map +1 -0
- package/lib/consentMonitor/index.d.ts +19 -0
- package/lib/consentMonitor/index.d.ts.map +1 -0
- package/lib/consentMonitor/index.js +117 -0
- package/lib/consentMonitor/index.js.map +1 -0
- package/lib/gaTracker/index.d.ts +13 -0
- package/lib/gaTracker/index.d.ts.map +1 -0
- package/lib/gaTracker/index.js +120 -0
- package/lib/gaTracker/index.js.map +1 -0
- package/lib/index.d.ts +9 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +18 -0
- package/lib/index.js.map +1 -0
- package/lib/oTracker/index.d.ts +14 -0
- package/lib/oTracker/index.d.ts.map +1 -0
- package/lib/oTracker/index.js +114 -0
- package/lib/oTracker/index.js.map +1 -0
- package/lib/permutiveVideoUtils/index.d.ts +12 -0
- package/lib/permutiveVideoUtils/index.d.ts.map +1 -0
- package/lib/permutiveVideoUtils/index.js +59 -0
- package/lib/permutiveVideoUtils/index.js.map +1 -0
- package/lib/reactPlayerTracking/index.d.ts +30 -0
- package/lib/reactPlayerTracking/index.d.ts.map +1 -0
- package/lib/reactPlayerTracking/index.js +85 -0
- package/lib/reactPlayerTracking/index.js.map +1 -0
- package/lib/utils/attention.d.ts +20 -0
- package/lib/utils/attention.d.ts.map +1 -0
- package/lib/utils/attention.js +142 -0
- package/lib/utils/attention.js.map +1 -0
- package/lib/utils/cookies.d.ts +2 -0
- package/lib/utils/cookies.d.ts.map +1 -0
- package/lib/utils/cookies.js +14 -0
- package/lib/utils/cookies.js.map +1 -0
- package/lib/utils/getTrace.d.ts +6 -0
- package/lib/utils/getTrace.d.ts.map +1 -0
- package/lib/utils/getTrace.js +109 -0
- package/lib/utils/getTrace.js.map +1 -0
- package/lib/utils/intersectionObserverPolyfill.d.ts +1 -0
- package/lib/utils/intersectionObserverPolyfill.d.ts.map +1 -0
- package/lib/utils/intersectionObserverPolyfill.js +421 -0
- package/lib/utils/intersectionObserverPolyfill.js.map +1 -0
- package/lib/utils/scroll.d.ts +18 -0
- package/lib/utils/scroll.d.ts.map +1 -0
- package/lib/utils/scroll.js +75 -0
- package/lib/utils/scroll.js.map +1 -0
- package/lib/utils/yupValidator.d.ts +87 -0
- package/lib/utils/yupValidator.d.ts.map +1 -0
- package/lib/utils/yupValidator.js +189 -0
- package/lib/utils/yupValidator.js.map +1 -0
- package/lib/ytIframeTracking/index.d.ts +27 -0
- package/lib/ytIframeTracking/index.d.ts.map +1 -0
- package/lib/ytIframeTracking/index.js +165 -0
- package/lib/ytIframeTracking/index.js.map +1 -0
- package/package.json +75 -0
- package/src/FTTracking/index.ts +74 -0
- package/src/consentMonitor/index.ts +120 -0
- package/src/gaTracker/index.ts +136 -0
- package/src/index.ts +12 -0
- package/src/oTracker/index.ts +122 -0
- package/src/permutiveVideoUtils/index.ts +53 -0
- package/src/reactPlayerTracking/index.ts +105 -0
- package/src/types.d.ts +33 -0
- package/src/utils/attention.js +138 -0
- package/src/utils/cookies.js +9 -0
- package/src/utils/getTrace.js +122 -0
- package/src/utils/intersectionObserverPolyfill.js +470 -0
- package/src/utils/scroll.ts +94 -0
- package/src/utils/yupValidator.ts +205 -0
- package/src/ytIframeTracking/index.ts +210 -0
- package/test/index.test.tsx +63 -0
- package/tsconfig.json +26 -0
- package/tsconfig.test.json +6 -0
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import * as oViewport from "@financial-times/o-viewport";
|
|
2
|
+
|
|
3
|
+
const ATTENTION_INTERVAL = 15000;
|
|
4
|
+
const ATTENTION_EVENTS = [
|
|
5
|
+
"load",
|
|
6
|
+
"click",
|
|
7
|
+
"focus",
|
|
8
|
+
"scroll",
|
|
9
|
+
"mousemove",
|
|
10
|
+
"touchstart",
|
|
11
|
+
"touchend",
|
|
12
|
+
"touchcancel",
|
|
13
|
+
"touchleave",
|
|
14
|
+
];
|
|
15
|
+
const UNATTENTION_EVENTS = ["blur"];
|
|
16
|
+
const EXIT_EVENTS = ["beforeunload", "unload", "pagehide"];
|
|
17
|
+
|
|
18
|
+
export class Attention {
|
|
19
|
+
totalAttentionTime = 0;
|
|
20
|
+
startAttentionTime;
|
|
21
|
+
endAttentionTime;
|
|
22
|
+
hasSentEvent = false;
|
|
23
|
+
oTracker;
|
|
24
|
+
|
|
25
|
+
constructor(oTracker) {
|
|
26
|
+
this.oTracker = oTracker;
|
|
27
|
+
this.init();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
init() {
|
|
31
|
+
//Add events for all the other Attention events
|
|
32
|
+
for (let i = 0; i < ATTENTION_EVENTS.length; i++) {
|
|
33
|
+
window.addEventListener(ATTENTION_EVENTS[i], (ev) =>
|
|
34
|
+
this.startAttention(ev)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < UNATTENTION_EVENTS.length; i++) {
|
|
39
|
+
window.addEventListener(UNATTENTION_EVENTS[i], (ev) =>
|
|
40
|
+
this.endAttention(ev)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Need to wait for this to be available
|
|
45
|
+
oViewport.listenTo("visibility");
|
|
46
|
+
document.body.addEventListener(
|
|
47
|
+
"oViewport.visibility",
|
|
48
|
+
(ev) => this.handleVisibilityChange(ev),
|
|
49
|
+
false
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
this.addVideoEvents();
|
|
53
|
+
|
|
54
|
+
// Add event to send data on unload
|
|
55
|
+
EXIT_EVENTS.forEach((event) => {
|
|
56
|
+
window.addEventListener(event, () => {
|
|
57
|
+
if (this.hasSentEvent) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
this.hasSentEvent = true;
|
|
61
|
+
this.endAttention();
|
|
62
|
+
this.oTracker.eventDispatcher({
|
|
63
|
+
category: "page",
|
|
64
|
+
action: "interaction",
|
|
65
|
+
context: {
|
|
66
|
+
attention: {
|
|
67
|
+
total: this.totalAttentionTime,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
startAttention() {
|
|
76
|
+
clearTimeout(this.attentionTimeout);
|
|
77
|
+
if (!this.startAttentionTime) {
|
|
78
|
+
this.startAttentionTime = new Date().getTime();
|
|
79
|
+
}
|
|
80
|
+
this.attentionTimeout = setTimeout(
|
|
81
|
+
() => this.endAttention(),
|
|
82
|
+
ATTENTION_INTERVAL
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
startConstantAttention() {
|
|
87
|
+
this.constantAttentionInterval = setInterval(
|
|
88
|
+
() => this.startAttention(),
|
|
89
|
+
ATTENTION_INTERVAL
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
endConstantAttention() {
|
|
94
|
+
this.endAttention();
|
|
95
|
+
clearInterval(this.constantAttentionInterval);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
endAttention() {
|
|
99
|
+
if (this.startAttentionTime) {
|
|
100
|
+
this.endAttentionTime = new Date().getTime();
|
|
101
|
+
this.totalAttentionTime += Math.round(
|
|
102
|
+
(this.endAttentionTime - this.startAttentionTime) / 1000
|
|
103
|
+
);
|
|
104
|
+
clearTimeout(this.attentionTimeout);
|
|
105
|
+
this.startAttentionTime = null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
get() {
|
|
110
|
+
//getter should restart attention capturing as endAttention updates the value:
|
|
111
|
+
this.endAttention();
|
|
112
|
+
this.startAttention();
|
|
113
|
+
return this.totalAttentionTime;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
addVideoEvents() {
|
|
117
|
+
this.videoPlayers = document.getElementsByTagName("video");
|
|
118
|
+
for (let i = 0; i < this.videoPlayers.length; i++) {
|
|
119
|
+
this.videoPlayers[i].addEventListener("playing", (ev) =>
|
|
120
|
+
this.startConstantAttention(ev)
|
|
121
|
+
);
|
|
122
|
+
this.videoPlayers[i].addEventListener("pause", (ev) =>
|
|
123
|
+
this.endConstantAttention(ev)
|
|
124
|
+
);
|
|
125
|
+
this.videoPlayers[i].addEventListener("ended", (ev) =>
|
|
126
|
+
this.endConstantAttention(ev)
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
handleVisibilityChange(ev) {
|
|
132
|
+
if (ev.detail.hidden) {
|
|
133
|
+
this.endAttention();
|
|
134
|
+
} else {
|
|
135
|
+
this.startAttention();
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
function sanitise(str) {
|
|
2
|
+
return typeof str === "string" ? str.trim() : str;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
function assignIfUndefined(subject, target) {
|
|
6
|
+
for (const prop in subject) {
|
|
7
|
+
if (!target[prop]) {
|
|
8
|
+
target[prop] = subject[prop];
|
|
9
|
+
} else {
|
|
10
|
+
console.warn(`You can't set a custom property called ${prop}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const elementPropertiesToCollect = [
|
|
16
|
+
"nodeName",
|
|
17
|
+
"className",
|
|
18
|
+
"id",
|
|
19
|
+
"href",
|
|
20
|
+
"text",
|
|
21
|
+
"role",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// For a given container element, get the number of elements that match the
|
|
25
|
+
// original element (siblings); and the index of the original element (position).
|
|
26
|
+
const getSiblingsAndPosition = (el, originalEl, selector) => {
|
|
27
|
+
const siblings = Array.from(el.querySelectorAll(selector));
|
|
28
|
+
const position = siblings.findIndex((item) => item === originalEl);
|
|
29
|
+
if (position === -1) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
siblings: siblings.length,
|
|
34
|
+
position,
|
|
35
|
+
};
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Get all (sanitised) properties of a given element.
|
|
39
|
+
const getAllElementProperties = (el) =>
|
|
40
|
+
elementPropertiesToCollect.reduce((returnObject, property) => {
|
|
41
|
+
if (el[property]) {
|
|
42
|
+
returnObject[property] = sanitise(el[property]);
|
|
43
|
+
} else if (el.getAttribute(property)) {
|
|
44
|
+
returnObject[property] = sanitise(el.getAttribute(property));
|
|
45
|
+
} else if (el.hasAttribute(property)) {
|
|
46
|
+
returnObject[property] = el.hasAttribute(property);
|
|
47
|
+
}
|
|
48
|
+
return returnObject;
|
|
49
|
+
}, {});
|
|
50
|
+
|
|
51
|
+
// Get some properties of a given element.
|
|
52
|
+
const getDomPathProps = (attrs, props) => {
|
|
53
|
+
// Collect any attribute that matches given strings.
|
|
54
|
+
attrs
|
|
55
|
+
.filter((attribute) =>
|
|
56
|
+
attribute.name.match(/^data-trackable|^data-o-|^aria-/i)
|
|
57
|
+
)
|
|
58
|
+
.forEach((attribute) => {
|
|
59
|
+
props[attribute.name] = attribute.value;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
return props;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Get only the custom data-trackable-context-? properties of a given element
|
|
66
|
+
const getContextProps = (attrs, props, isOriginalEl) => {
|
|
67
|
+
const customProps = {};
|
|
68
|
+
|
|
69
|
+
// for the original element collect properties like className, nodeName
|
|
70
|
+
if (isOriginalEl) {
|
|
71
|
+
elementPropertiesToCollect.forEach((name) => {
|
|
72
|
+
if (typeof props[name] !== "undefined" && name !== "id") {
|
|
73
|
+
customProps[name] = props[name];
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Collect any attribute that matches given strings.
|
|
79
|
+
attrs
|
|
80
|
+
.filter((attribute) => attribute.name.match(/^data-trackable-context-/i))
|
|
81
|
+
.forEach((attribute) => {
|
|
82
|
+
customProps[attribute.name.replace("data-trackable-context-", "")] =
|
|
83
|
+
attribute.value;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return customProps;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function getTrace(el) {
|
|
90
|
+
const rootEl = document;
|
|
91
|
+
const originalEl = el;
|
|
92
|
+
const selector = originalEl.getAttribute("data-trackable")
|
|
93
|
+
? `[data-trackable="${originalEl.getAttribute("data-trackable")}"]`
|
|
94
|
+
: originalEl.nodeName;
|
|
95
|
+
const trace = [];
|
|
96
|
+
const customContext = {};
|
|
97
|
+
while (el && el !== rootEl) {
|
|
98
|
+
const props = getAllElementProperties(el);
|
|
99
|
+
const attrs = Array.from(el.attributes);
|
|
100
|
+
let domPathProps = getDomPathProps(attrs, props);
|
|
101
|
+
|
|
102
|
+
// If the element happens to have a data-trackable attribute, get the siblings
|
|
103
|
+
// and position of the element (relative to the current element).
|
|
104
|
+
if (domPathProps["data-trackable"]) {
|
|
105
|
+
domPathProps = Object.assign(
|
|
106
|
+
domPathProps,
|
|
107
|
+
getSiblingsAndPosition(el, originalEl, selector)
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
trace.push(domPathProps);
|
|
112
|
+
|
|
113
|
+
const contextProps = getContextProps(attrs, props, el === originalEl);
|
|
114
|
+
|
|
115
|
+
assignIfUndefined(contextProps, customContext);
|
|
116
|
+
|
|
117
|
+
el = el.parentNode;
|
|
118
|
+
}
|
|
119
|
+
return { trace, customContext };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export default getTrace;
|