@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,120 @@
|
|
|
1
|
+
import Debug from "debug";
|
|
2
|
+
const debug = Debug("@phntms/ft-lib");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_DEV_HOSTS = ["localhost", "phq", ".app", "preview"];
|
|
5
|
+
|
|
6
|
+
export class consentMonitor {
|
|
7
|
+
protected _consent = false;
|
|
8
|
+
protected _devHosts: string[];
|
|
9
|
+
protected _isDevEnvironment = false;
|
|
10
|
+
protected _hostname: string;
|
|
11
|
+
protected _isInitialized = false;
|
|
12
|
+
|
|
13
|
+
constructor(hostname?: string, devHosts?: string[] | string) {
|
|
14
|
+
if (Array.isArray(devHosts)) {
|
|
15
|
+
this._devHosts = devHosts.concat(DEFAULT_DEV_HOSTS);
|
|
16
|
+
} else if (devHosts === undefined) {
|
|
17
|
+
this._devHosts = DEFAULT_DEV_HOSTS;
|
|
18
|
+
} else {
|
|
19
|
+
this._devHosts = DEFAULT_DEV_HOSTS;
|
|
20
|
+
this._devHosts.push(devHosts);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
this._hostname = hostname || window.location.hostname;
|
|
24
|
+
this.init();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get consent(): boolean {
|
|
28
|
+
return this._consent;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get devHosts(): string[] | string {
|
|
32
|
+
return this._devHosts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get isDevEnvironment(): boolean {
|
|
36
|
+
return this._isDevEnvironment;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get isInitialized(): boolean {
|
|
40
|
+
return this._isInitialized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getCookieValue = (name: string) =>
|
|
44
|
+
document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")?.pop() || "";
|
|
45
|
+
|
|
46
|
+
init = () => {
|
|
47
|
+
this.cookieConsentTest();
|
|
48
|
+
setInterval(this.cookieConsentTest, 3000);
|
|
49
|
+
|
|
50
|
+
//Simulate cookie consent behaviour in non-prod environments
|
|
51
|
+
this._devHosts.map(
|
|
52
|
+
(devHost) =>
|
|
53
|
+
this._hostname.includes(devHost) && this.setDevCookieHandler()
|
|
54
|
+
);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
cookieConsentTest = () => {
|
|
58
|
+
if (window.permutive) {
|
|
59
|
+
if (!this._isInitialized) {
|
|
60
|
+
if (
|
|
61
|
+
this.getCookieValue("FTConsent").includes("behaviouraladsOnsite%3Aon")
|
|
62
|
+
) {
|
|
63
|
+
this.permutiveConsentOn();
|
|
64
|
+
} else {
|
|
65
|
+
this.permutiveConsentOff();
|
|
66
|
+
}
|
|
67
|
+
this._isInitialized = true;
|
|
68
|
+
} else if (
|
|
69
|
+
this.getCookieValue("FTConsent").includes(
|
|
70
|
+
"behaviouraladsOnsite%3Aon"
|
|
71
|
+
) &&
|
|
72
|
+
!this.consent
|
|
73
|
+
) {
|
|
74
|
+
debug("setting permutive tracking consent: on");
|
|
75
|
+
this.permutiveConsentOn();
|
|
76
|
+
} else if (
|
|
77
|
+
!this.getCookieValue("FTConsent").includes(
|
|
78
|
+
"behaviouraladsOnsite%3Aon"
|
|
79
|
+
) &&
|
|
80
|
+
this.consent
|
|
81
|
+
) {
|
|
82
|
+
debug("setting permutive tracking consent: off");
|
|
83
|
+
this.permutiveConsentOff();
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
setDevCookieHandler = () => {
|
|
89
|
+
this._isDevEnvironment = true;
|
|
90
|
+
debug("setting development environment from host match");
|
|
91
|
+
const oCookieMessage =
|
|
92
|
+
document.getElementsByClassName("o-cookie-message")[0];
|
|
93
|
+
if (oCookieMessage) {
|
|
94
|
+
const onCookieMessageAct = () => {
|
|
95
|
+
debug("setting development FT consent cookies");
|
|
96
|
+
document.cookie = "FTConsent=behaviouraladsOnsite%3Aon";
|
|
97
|
+
document.cookie = "FTCookieConsentGDPR=true";
|
|
98
|
+
oCookieMessage.removeEventListener(
|
|
99
|
+
"oCookieMessage.act",
|
|
100
|
+
onCookieMessageAct,
|
|
101
|
+
false
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
oCookieMessage.addEventListener("oCookieMessage.act", onCookieMessageAct);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
permutiveConsentOn = () => {
|
|
109
|
+
window.permutive.consent({
|
|
110
|
+
opt_in: true,
|
|
111
|
+
token: "behaviouraladsOnsite:on",
|
|
112
|
+
});
|
|
113
|
+
this._consent = true;
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
permutiveConsentOff = () => {
|
|
117
|
+
window.permutive.consent({ opt_in: false });
|
|
118
|
+
this._consent = false;
|
|
119
|
+
};
|
|
120
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { TrackingOptions } from "../FTTracking";
|
|
2
|
+
import { validateGTMCustomEvent } from "../utils/yupValidator";
|
|
3
|
+
const HEADER_CATEGORY = "Header";
|
|
4
|
+
const HEADER_SEARCH_ACTION = "Engagement - Search";
|
|
5
|
+
|
|
6
|
+
export class gaTracker {
|
|
7
|
+
options;
|
|
8
|
+
constructor(options: TrackingOptions) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.setUpFTNavigationLinksTracking();
|
|
11
|
+
this.addDataAttributesListener();
|
|
12
|
+
this.setUpFTNavigationSearchTracking(HEADER_CATEGORY, HEADER_SEARCH_ACTION);
|
|
13
|
+
this.setupFooterLinksTracking();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
public GTMEventDispatcher(category: string, action: string, label: string) {
|
|
17
|
+
validateGTMCustomEvent({ category, action, label });
|
|
18
|
+
//from channels - sets event categories on window on certain pages..replace?
|
|
19
|
+
if (category === "<category>" && window.gtmCategory) {
|
|
20
|
+
category = window.gtmCategory;
|
|
21
|
+
}
|
|
22
|
+
window.dataLayer.push({
|
|
23
|
+
event: "customEvent",
|
|
24
|
+
category,
|
|
25
|
+
action,
|
|
26
|
+
label,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public GTMVirtualPageView(pageUrl: string, pageTitle: string) {
|
|
31
|
+
window.dataLayer.push({
|
|
32
|
+
event: "virtualPageView",
|
|
33
|
+
pageUrl,
|
|
34
|
+
pageTitle,
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public GtagEventDispatcher(category: string, action: string, label: string) {
|
|
39
|
+
validateGTMCustomEvent({ category, action, label });
|
|
40
|
+
window.gtag("event", action, {
|
|
41
|
+
event_category: category,
|
|
42
|
+
event_label: label,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
addDataAttributesListener() {
|
|
47
|
+
document.body.addEventListener("click", (evt) => {
|
|
48
|
+
//get nearest element with data-gadl attribute
|
|
49
|
+
const elm = (evt.target as HTMLElement).closest("[data-gadl]");
|
|
50
|
+
const gaEventString = elm?.getAttribute("data-gadl");
|
|
51
|
+
if (!gaEventString) return;
|
|
52
|
+
const [category, action, label] = gaEventString.split("|");
|
|
53
|
+
|
|
54
|
+
//TODO validate event fields
|
|
55
|
+
if (typeof window !== "undefined") {
|
|
56
|
+
//TBC - GTM customEvent tag vs standard UA event push?
|
|
57
|
+
if (this.options.isCustomGTMEvent) {
|
|
58
|
+
this.GTMEventDispatcher(
|
|
59
|
+
category,
|
|
60
|
+
action,
|
|
61
|
+
label || window.location.pathname //use pathname if label section has not been defined
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
this.GtagEventDispatcher(
|
|
65
|
+
category,
|
|
66
|
+
action,
|
|
67
|
+
label || window.location.pathname
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
setUpFTNavigationLinksTracking() {
|
|
75
|
+
const FTNavigationLinks = Array.from(
|
|
76
|
+
document.querySelectorAll(".o-header__drawer-menu-link")
|
|
77
|
+
);
|
|
78
|
+
if (FTNavigationLinks) {
|
|
79
|
+
FTNavigationLinks.map((navLink) => {
|
|
80
|
+
const trackingValue = `Header|Engagement - Hamburger|${
|
|
81
|
+
document.URL
|
|
82
|
+
} - ${(navLink as HTMLAnchorElement).href}`;
|
|
83
|
+
navLink.setAttribute("data-gadl", trackingValue);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setUpFTNavigationSearchTracking(category: string, action: string) {
|
|
89
|
+
const searchForm = document.querySelector(".o-header__search-form");
|
|
90
|
+
if (searchForm) {
|
|
91
|
+
searchForm.addEventListener("submit", (event) => {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
const searchBarContent = (
|
|
94
|
+
document.querySelector(".o-header__search-term") as HTMLInputElement
|
|
95
|
+
)?.value;
|
|
96
|
+
if (typeof window !== "undefined") {
|
|
97
|
+
if (this.options.isCustomGTMEvent) {
|
|
98
|
+
window.dataLayer.push({
|
|
99
|
+
event: "customEvent",
|
|
100
|
+
category,
|
|
101
|
+
action: `${action}: ${searchBarContent}`,
|
|
102
|
+
label: window.location.pathname,
|
|
103
|
+
eventCallback: () => {
|
|
104
|
+
(searchForm as HTMLFormElement).submit();
|
|
105
|
+
},
|
|
106
|
+
eventTimeout: 2000,
|
|
107
|
+
});
|
|
108
|
+
} else {
|
|
109
|
+
window.gtag("event", action, {
|
|
110
|
+
event_category: category,
|
|
111
|
+
event_label: searchBarContent,
|
|
112
|
+
event_callback: () => {
|
|
113
|
+
(searchForm as HTMLFormElement).submit();
|
|
114
|
+
},
|
|
115
|
+
event_timeout: 2000,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
setupFooterLinksTracking() {
|
|
124
|
+
const FTFooterLinks = Array.from(
|
|
125
|
+
document.querySelectorAll(".o-footer__legal-links li a")
|
|
126
|
+
);
|
|
127
|
+
if (FTFooterLinks) {
|
|
128
|
+
FTFooterLinks.forEach((footerLink) => {
|
|
129
|
+
const trackingValue = `Footer|Click - Footer Nav - ${
|
|
130
|
+
(footerLink as HTMLAnchorElement).href
|
|
131
|
+
}|${document.URL}`;
|
|
132
|
+
footerLink.setAttribute("data-gadl", trackingValue);
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export { consentMonitor } from "./consentMonitor";
|
|
2
|
+
export { permutiveVideoUtils } from "./permutiveVideoUtils";
|
|
3
|
+
export { reactPlayerTracking } from "./reactPlayerTracking";
|
|
4
|
+
export { gaTracker } from "./gaTracker";
|
|
5
|
+
export { oTracker } from "./oTracker";
|
|
6
|
+
export { ytIframeTracking } from "./ytIframeTracking";
|
|
7
|
+
export { FTTracking, TrackingOptions } from "./FTTracking";
|
|
8
|
+
export {
|
|
9
|
+
ConfigType,
|
|
10
|
+
OrigamiEventType,
|
|
11
|
+
GTMCustomEventType,
|
|
12
|
+
} from "./utils/yupValidator";
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import oTracking from "@financial-times/o-tracking";
|
|
2
|
+
|
|
3
|
+
import { TrackingOptions } from "../FTTracking";
|
|
4
|
+
import { getValueFromCookie } from "../utils/cookies";
|
|
5
|
+
import getTrace from "../utils/getTrace";
|
|
6
|
+
import {
|
|
7
|
+
parseConfig,
|
|
8
|
+
validateConfig,
|
|
9
|
+
validateOrigamiEvent,
|
|
10
|
+
ConfigType,
|
|
11
|
+
OrigamiEventType,
|
|
12
|
+
} from "../utils/yupValidator";
|
|
13
|
+
|
|
14
|
+
/* oTracking setup for server-rendered (i.e. Wagtail) sites
|
|
15
|
+
The config object is read from the server-rendered o-tracking-data element
|
|
16
|
+
*/
|
|
17
|
+
export class oTracker {
|
|
18
|
+
private _config: ConfigType;
|
|
19
|
+
options: TrackingOptions;
|
|
20
|
+
|
|
21
|
+
constructor(config: ConfigType, options: TrackingOptions) {
|
|
22
|
+
this._config = parseConfig(config);
|
|
23
|
+
validateConfig(this._config);
|
|
24
|
+
this._config.source_id = oTracking.getRootID();
|
|
25
|
+
this.options = options;
|
|
26
|
+
|
|
27
|
+
//init config data
|
|
28
|
+
const configData = {
|
|
29
|
+
server: "https://spoor-api.ft.com/px.gif",
|
|
30
|
+
context: {
|
|
31
|
+
...this._config,
|
|
32
|
+
},
|
|
33
|
+
user: {
|
|
34
|
+
ft_session: getValueFromCookie(/FTSession=([^;]+)/),
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
oTracking.init(configData);
|
|
38
|
+
|
|
39
|
+
//setup listeners
|
|
40
|
+
this.OTrackingHandler();
|
|
41
|
+
|
|
42
|
+
//send Origami DomLoaded event
|
|
43
|
+
//document.dispatchEvent(new CustomEvent("o.DOMContentLoaded")); //TODO this isnt needed on next and breaks the cookie-message eventhandlers - is it needed for the server sites?
|
|
44
|
+
//send page hits
|
|
45
|
+
this.broadcastPageView();
|
|
46
|
+
this.broadcastBrandedContent();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
set config(c: ConfigType) {
|
|
50
|
+
this._config = c;
|
|
51
|
+
}
|
|
52
|
+
get config() {
|
|
53
|
+
return this._config;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public eventDispatcher(detail: OrigamiEventType) {
|
|
57
|
+
if (typeof detail === "object" && detail.category && detail.action) {
|
|
58
|
+
validateOrigamiEvent(detail);
|
|
59
|
+
detail.app = this.config.app;
|
|
60
|
+
detail.product = this.config.product;
|
|
61
|
+
document.body.dispatchEvent(
|
|
62
|
+
new CustomEvent("oTracking.event", { detail, bubbles: true })
|
|
63
|
+
);
|
|
64
|
+
} else {
|
|
65
|
+
throw "Invalid event type";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
public broadcastPageView() {
|
|
70
|
+
oTracking.page({
|
|
71
|
+
app: this.config.app,
|
|
72
|
+
product: this.config.product,
|
|
73
|
+
title: document.title,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public broadcastBrandedContent() {
|
|
78
|
+
document.body.dispatchEvent(
|
|
79
|
+
new CustomEvent("oTracking.event", {
|
|
80
|
+
detail: {
|
|
81
|
+
...this.config,
|
|
82
|
+
action: "view",
|
|
83
|
+
category: "brandedContent",
|
|
84
|
+
},
|
|
85
|
+
bubbles: true,
|
|
86
|
+
})
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
OTrackingHandler() {
|
|
91
|
+
document.body.addEventListener("click", (event) => {
|
|
92
|
+
const trackableElement = event
|
|
93
|
+
.composedPath()
|
|
94
|
+
.find(
|
|
95
|
+
(elm) =>
|
|
96
|
+
elm instanceof HTMLElement &&
|
|
97
|
+
elm.hasAttribute &&
|
|
98
|
+
elm.hasAttribute("data-o-event")
|
|
99
|
+
);
|
|
100
|
+
if (trackableElement && trackableElement instanceof HTMLElement) {
|
|
101
|
+
const data = trackableElement.getAttribute("data-o-event");
|
|
102
|
+
const detail = {};
|
|
103
|
+
const { trace } = getTrace(trackableElement);
|
|
104
|
+
|
|
105
|
+
if (data) {
|
|
106
|
+
data.split("|").forEach((item) => {
|
|
107
|
+
const kv = item.split(":");
|
|
108
|
+
//TODO
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
detail[kv[0]] = kv[1];
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
//TODO
|
|
114
|
+
// @ts-ignore
|
|
115
|
+
detail.domPathTokens = trace;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
this.eventDispatcher(detail as OrigamiEventType);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Debug from "debug";
|
|
2
|
+
const debug = Debug("@phntms/ft-lib");
|
|
3
|
+
export class permutiveVideoUtils {
|
|
4
|
+
protected progressPermutiveMilestones: number[] = [0, 0.25, 0.5, 0.75, 1];
|
|
5
|
+
protected _campaign: string;
|
|
6
|
+
protected _title: string;
|
|
7
|
+
protected _videoId;
|
|
8
|
+
|
|
9
|
+
constructor(campaign: string, title: string, videoId = "") {
|
|
10
|
+
this._campaign = campaign;
|
|
11
|
+
this._title = title;
|
|
12
|
+
this._videoId = videoId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
set videoId(vid: string) {
|
|
16
|
+
this._videoId = vid;
|
|
17
|
+
}
|
|
18
|
+
get videoId() {
|
|
19
|
+
return this._videoId;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get remainingProgress(): number[] {
|
|
23
|
+
return this.progressPermutiveMilestones;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
emitPermutiveProgressEvent = (
|
|
27
|
+
duration: number,
|
|
28
|
+
currentTime: number,
|
|
29
|
+
interval: any = undefined
|
|
30
|
+
) => {
|
|
31
|
+
const progress = currentTime / (Math.floor(duration) - 1);
|
|
32
|
+
|
|
33
|
+
while (
|
|
34
|
+
progress >= this.progressPermutiveMilestones[0] &&
|
|
35
|
+
currentTime > 0 &&
|
|
36
|
+
window.permutive
|
|
37
|
+
) {
|
|
38
|
+
debug("Emit videoEngagement permutive event");
|
|
39
|
+
window.permutive.track("VideoEngagement", {
|
|
40
|
+
campaign: this._campaign,
|
|
41
|
+
createdAt: new Date().toISOString(),
|
|
42
|
+
duration: Math.floor(duration),
|
|
43
|
+
title: this._title,
|
|
44
|
+
videoId: this._videoId,
|
|
45
|
+
progress: this.progressPermutiveMilestones[0],
|
|
46
|
+
});
|
|
47
|
+
this.progressPermutiveMilestones.shift();
|
|
48
|
+
if (this.progressPermutiveMilestones.length === 0 && interval) {
|
|
49
|
+
window.clearInterval(interval);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import Debug from "debug";
|
|
2
|
+
|
|
3
|
+
import { FTTracking } from "../FTTracking";
|
|
4
|
+
import { permutiveVideoUtils } from "../permutiveVideoUtils";
|
|
5
|
+
import { OrigamiEventType } from "../utils/yupValidator";
|
|
6
|
+
const debug = Debug("@phntms/ft-lib");
|
|
7
|
+
interface PlayerProgressEvent {
|
|
8
|
+
played: number;
|
|
9
|
+
loaded: number;
|
|
10
|
+
playedSeconds: number;
|
|
11
|
+
loadedSeconds: number;
|
|
12
|
+
}
|
|
13
|
+
export class reactPlayerTracking {
|
|
14
|
+
duration = 0;
|
|
15
|
+
playedSeconds = 0;
|
|
16
|
+
playedPercent = 0;
|
|
17
|
+
videoTitle: string;
|
|
18
|
+
videoUrl: string;
|
|
19
|
+
|
|
20
|
+
/** 100% is fired with ended event */
|
|
21
|
+
GA_milestones: number[] = [1, 25, 50, 75];
|
|
22
|
+
oTracking_milestones: number[] = [25, 50, 75];
|
|
23
|
+
FTTracker: FTTracking;
|
|
24
|
+
permutiveTracker: permutiveVideoUtils | undefined;
|
|
25
|
+
|
|
26
|
+
constructor(FTTracker: FTTracking, videoTitle: string, videoUrl: string) {
|
|
27
|
+
/** oTracking dispatch event function */
|
|
28
|
+
this.FTTracker = FTTracker;
|
|
29
|
+
this.videoTitle = videoTitle;
|
|
30
|
+
this.videoUrl = videoUrl;
|
|
31
|
+
this.permutiveTracker = new permutiveVideoUtils(
|
|
32
|
+
this.FTTracker.config.campaign,
|
|
33
|
+
this.videoTitle,
|
|
34
|
+
this.videoUrl
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
setDuration = (duration: number) => {
|
|
39
|
+
this.duration = Math.floor(duration);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
sendGAEvent = (action: string) => {
|
|
43
|
+
debug("GA datalayer push - " + action);
|
|
44
|
+
this.FTTracker.gaEvent("Video", action, this.videoTitle);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
sendoTrackingEvent = (action: string, progress?: number) => {
|
|
48
|
+
debug("oTracking event - " + action);
|
|
49
|
+
this.FTTracker.oEvent({
|
|
50
|
+
category: "video",
|
|
51
|
+
action: action,
|
|
52
|
+
product: this.FTTracker.config.product,
|
|
53
|
+
app: this.FTTracker.config.app,
|
|
54
|
+
duration: this.duration,
|
|
55
|
+
...(progress && { progress: progress }),
|
|
56
|
+
} as OrigamiEventType);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
trackPlay = () => {
|
|
60
|
+
this.sendGAEvent("playing");
|
|
61
|
+
this.sendoTrackingEvent("playing");
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
trackPause = () => {
|
|
65
|
+
this.sendGAEvent("pause");
|
|
66
|
+
this.sendoTrackingEvent("pause");
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
trackEnded = () => {
|
|
70
|
+
//fire 100% progress events
|
|
71
|
+
this.sendGAEvent("100% watched");
|
|
72
|
+
this.sendoTrackingEvent("progress", 100);
|
|
73
|
+
|
|
74
|
+
this.sendGAEvent("ended");
|
|
75
|
+
this.sendoTrackingEvent("ended", 100);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
trackProgress = (reactPlayerProgress: PlayerProgressEvent) => {
|
|
79
|
+
this.playedSeconds = reactPlayerProgress.playedSeconds;
|
|
80
|
+
if (this.duration > 0) {
|
|
81
|
+
this.playedPercent = Math.floor(
|
|
82
|
+
(Math.ceil(reactPlayerProgress.playedSeconds) / this.duration) * 100
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
while (this.playedPercent >= this.GA_milestones[0]) {
|
|
87
|
+
debug("GA progress milestone - " + this.GA_milestones[0]);
|
|
88
|
+
this.sendGAEvent(`${this.GA_milestones[0]}% watched`);
|
|
89
|
+
this.GA_milestones.shift();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
while (this.playedPercent >= this.oTracking_milestones[0]) {
|
|
93
|
+
debug("oTracking progress milestone - " + this.oTracking_milestones[0]);
|
|
94
|
+
this.sendoTrackingEvent("progress", this.oTracking_milestones[0]);
|
|
95
|
+
this.oTracking_milestones.shift();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//permutive only tracks progress events and contains its own milestones
|
|
99
|
+
debug("edit permutive progress event- " + this.playedSeconds);
|
|
100
|
+
this.permutiveTracker?.emitPermutiveProgressEvent(
|
|
101
|
+
this.duration,
|
|
102
|
+
this.playedSeconds
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
declare module "@financial-times/o-tracking";
|
|
2
|
+
declare module "@financial-times/o-viewport";
|
|
3
|
+
|
|
4
|
+
interface Window {
|
|
5
|
+
dataLayer: any;
|
|
6
|
+
gtag: any;
|
|
7
|
+
permutive: {
|
|
8
|
+
addon: any;
|
|
9
|
+
consent: CallableConsent;
|
|
10
|
+
track: any;
|
|
11
|
+
};
|
|
12
|
+
gtmCategory?: string; //channels only
|
|
13
|
+
isOvideoPlayer?: boolean; //channels only
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface ConsentOptions {
|
|
17
|
+
opt_in?: boolean;
|
|
18
|
+
token?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface CallableConsent {
|
|
22
|
+
({}: ConsentOptions): void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface VideoTrackingOptions {
|
|
26
|
+
isOTracking?: boolean;
|
|
27
|
+
isGATracking?: boolean;
|
|
28
|
+
isPermutiveTracking?: boolean;
|
|
29
|
+
GA_datalayer?: any;
|
|
30
|
+
GA_milestones?: number[];
|
|
31
|
+
oTracking_milestones?: number[];
|
|
32
|
+
route_url?: string;
|
|
33
|
+
}
|