@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.
Files changed (88) hide show
  1. package/.babelrc +6 -0
  2. package/.eslintrc +58 -0
  3. package/.github/CODEOWNERS +1 -0
  4. package/.github/workflows/pr.yml +31 -0
  5. package/.github/workflows/release.yml +44 -0
  6. package/.nvmrc +1 -0
  7. package/.prettierignore +5 -0
  8. package/CODE_OF_CONDUCT.md +76 -0
  9. package/CONTRIBUTING.md +48 -0
  10. package/LICENSE +21 -0
  11. package/README.md +158 -0
  12. package/SUPPORT.md +4 -0
  13. package/jest.config.js +24 -0
  14. package/lib/FTTracking/index.d.ts +24 -0
  15. package/lib/FTTracking/index.d.ts.map +1 -0
  16. package/lib/FTTracking/index.js +69 -0
  17. package/lib/FTTracking/index.js.map +1 -0
  18. package/lib/consentMonitor/index.d.ts +19 -0
  19. package/lib/consentMonitor/index.d.ts.map +1 -0
  20. package/lib/consentMonitor/index.js +117 -0
  21. package/lib/consentMonitor/index.js.map +1 -0
  22. package/lib/gaTracker/index.d.ts +13 -0
  23. package/lib/gaTracker/index.d.ts.map +1 -0
  24. package/lib/gaTracker/index.js +120 -0
  25. package/lib/gaTracker/index.js.map +1 -0
  26. package/lib/index.d.ts +9 -0
  27. package/lib/index.d.ts.map +1 -0
  28. package/lib/index.js +18 -0
  29. package/lib/index.js.map +1 -0
  30. package/lib/oTracker/index.d.ts +14 -0
  31. package/lib/oTracker/index.d.ts.map +1 -0
  32. package/lib/oTracker/index.js +114 -0
  33. package/lib/oTracker/index.js.map +1 -0
  34. package/lib/permutiveVideoUtils/index.d.ts +12 -0
  35. package/lib/permutiveVideoUtils/index.d.ts.map +1 -0
  36. package/lib/permutiveVideoUtils/index.js +59 -0
  37. package/lib/permutiveVideoUtils/index.js.map +1 -0
  38. package/lib/reactPlayerTracking/index.d.ts +30 -0
  39. package/lib/reactPlayerTracking/index.d.ts.map +1 -0
  40. package/lib/reactPlayerTracking/index.js +85 -0
  41. package/lib/reactPlayerTracking/index.js.map +1 -0
  42. package/lib/utils/attention.d.ts +20 -0
  43. package/lib/utils/attention.d.ts.map +1 -0
  44. package/lib/utils/attention.js +142 -0
  45. package/lib/utils/attention.js.map +1 -0
  46. package/lib/utils/cookies.d.ts +2 -0
  47. package/lib/utils/cookies.d.ts.map +1 -0
  48. package/lib/utils/cookies.js +14 -0
  49. package/lib/utils/cookies.js.map +1 -0
  50. package/lib/utils/getTrace.d.ts +6 -0
  51. package/lib/utils/getTrace.d.ts.map +1 -0
  52. package/lib/utils/getTrace.js +109 -0
  53. package/lib/utils/getTrace.js.map +1 -0
  54. package/lib/utils/intersectionObserverPolyfill.d.ts +1 -0
  55. package/lib/utils/intersectionObserverPolyfill.d.ts.map +1 -0
  56. package/lib/utils/intersectionObserverPolyfill.js +421 -0
  57. package/lib/utils/intersectionObserverPolyfill.js.map +1 -0
  58. package/lib/utils/scroll.d.ts +18 -0
  59. package/lib/utils/scroll.d.ts.map +1 -0
  60. package/lib/utils/scroll.js +75 -0
  61. package/lib/utils/scroll.js.map +1 -0
  62. package/lib/utils/yupValidator.d.ts +87 -0
  63. package/lib/utils/yupValidator.d.ts.map +1 -0
  64. package/lib/utils/yupValidator.js +189 -0
  65. package/lib/utils/yupValidator.js.map +1 -0
  66. package/lib/ytIframeTracking/index.d.ts +27 -0
  67. package/lib/ytIframeTracking/index.d.ts.map +1 -0
  68. package/lib/ytIframeTracking/index.js +165 -0
  69. package/lib/ytIframeTracking/index.js.map +1 -0
  70. package/package.json +75 -0
  71. package/src/FTTracking/index.ts +74 -0
  72. package/src/consentMonitor/index.ts +120 -0
  73. package/src/gaTracker/index.ts +136 -0
  74. package/src/index.ts +12 -0
  75. package/src/oTracker/index.ts +122 -0
  76. package/src/permutiveVideoUtils/index.ts +53 -0
  77. package/src/reactPlayerTracking/index.ts +105 -0
  78. package/src/types.d.ts +33 -0
  79. package/src/utils/attention.js +138 -0
  80. package/src/utils/cookies.js +9 -0
  81. package/src/utils/getTrace.js +122 -0
  82. package/src/utils/intersectionObserverPolyfill.js +470 -0
  83. package/src/utils/scroll.ts +94 -0
  84. package/src/utils/yupValidator.ts +205 -0
  85. package/src/ytIframeTracking/index.ts +210 -0
  86. package/test/index.test.tsx +63 -0
  87. package/tsconfig.json +26 -0
  88. 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
+ }