Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
v22.19.0
2,813 changes: 1,867 additions & 946 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@phantomstudios/ft-lib",
"description": "A collection of Javascript UI & tracking utils for FT sites",
"version": "0.4.1-rc1",
"version": "0.5.0-rc15",
"main": "lib/index.js",
"types": "lib/index.d.ts",
"homepage": "https://github.com/phantomstudios/ft-lib#readme",
Expand Down Expand Up @@ -41,6 +41,11 @@
"@eslint/compat": "^1.2.9",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.25.1",
"@financial-times/cmp-client": "^6.2.0",
"@financial-times/o-footer": "^9.2.10",
"@financial-times/o-header": "^11.2.0",
"@financial-times/o-tracking": "^4.9.0",
"@financial-times/o-viewport": "^5.1.2",
"@types/debug": "^4.1.7",
"@types/jest": "^29.5.14",
"@types/youtube": "^0.1.0",
Expand Down Expand Up @@ -69,6 +74,7 @@
"yup": "^1.0.2"
},
"peerDependencies": {
"@financial-times/cmp-client": "^6.1.2",
"@financial-times/o-tracking": "^4.5.1",
"@financial-times/o-viewport": "^5.1.2"
}
Expand Down
23 changes: 20 additions & 3 deletions src/FTTracking/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { consentMonitor } from "../consentMonitor";
import {
ConsentMonitor,
ConsentMonitor as consentMonitor,
} from "../consentMonitor";
import { gaTracker } from "../gaTracker";
import { oTracker } from "../oTracker";
import { ScrollTracker } from "../utils/scroll";
import {
validateConfig,
ConfigType,
OrigamiEventType,
validateConfig,
} from "../utils/yupValidator";

export interface TrackingOptions {
Expand All @@ -32,6 +35,7 @@ export class FTTracking {
scrollTracker: ScrollTracker;
disableAppFormatTransform: boolean;
logValidationErrors: boolean;
consentMonitor: ConsentMonitor | undefined;
oEvent: (detail: OrigamiEventType) => void;
gaEvent: (category: string, action: string, label: string) => void;
gtmEvent: (category: string, action: string, label: string) => void;
Expand All @@ -57,7 +61,10 @@ export class FTTracking {

//cookie consent monitor for permutive tracking
window.addEventListener("load", () => {
new consentMonitor(window.location.hostname, [".app", "preview"]);
this.consentMonitor = new consentMonitor(window.location.hostname, [
".app",
"preview",
]);
});
}
set config(c: ConfigType) {
Expand All @@ -68,6 +75,16 @@ export class FTTracking {
return this._config;
}

initializeConsentMonitor = () => {
if (!this.consentMonitor) {
this.consentMonitor = new consentMonitor(window.location.hostname, [
".app",
"preview",
]);
}
return this.consentMonitor;
};

public newPageView(config: ConfigType) {
//Update passed config to otracker,send pageview events and reset scrollTracker
validateConfig(
Expand Down
28 changes: 28 additions & 0 deletions src/cmp/loadFtCmp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export function loadFtCmpScript(): Promise<void> {
if (document.getElementById("ft-cmp-loader")) {
return Promise.resolve();
}

return new Promise<void>((resolve, reject) => {
const script: HTMLScriptElement = document.createElement("script");
script.id = "ft-cmp-loader";
script.async = true;
script.src = "https://consent-notice.ft.com/cmp.js";
script.referrerPolicy = window.location.hostname.endsWith(".ft.com")
? "" // production hosts
: "no-referrer-when-downgrade"; // localhost / preview

script.onload = () => resolve();
script.onerror = () =>
reject(new Error("Sourcepoint CMP script failed to load"));

document.head.appendChild(script);
});
}

export function enqueueCmpCallback(cb: () => void): void {
if (!window._sp_) window._sp_ = {};
if (!window._sp_queue) window._sp_queue = [];

window._sp_queue!.push(cb);
}
216 changes: 132 additions & 84 deletions src/consentMonitor/index.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,168 @@
import {
initSourcepointCmp,
interceptManageCookiesLinks,
properties,
} from "@financial-times/cmp-client";
import Debug from "debug";
const debug = Debug("@phantomstudios/ft-lib");

import { enqueueCmpCallback, loadFtCmpScript } from "../cmp/loadFtCmp";

const debug = Debug("@phantomstudios/ft-lib/consentMonitor");

const DEFAULT_DEV_HOSTS = ["localhost", "phq", ".app", "preview"];

export class consentMonitor {
protected _consent = false;
protected _devHosts: string[];
protected _isDevEnvironment = false;
protected _hostname: string;
protected _isInitialized = false;
interface ConsentReadyInfo {
consentedToAll: boolean;
}
type ConsentReadyHandler = (
legislation: string,
uuid: string,
tcData: unknown,
info: ConsentReadyInfo,
) => void;
type MessageChoiceHandler = (
legislation: string,
choiceId: number,
choiceTypeId: number,
) => void;

constructor(hostname?: string, devHosts?: string[] | string) {
if (Array.isArray(devHosts)) {
this._devHosts = devHosts.concat(DEFAULT_DEV_HOSTS);
} else if (devHosts === undefined) {
this._devHosts = DEFAULT_DEV_HOSTS;
} else {
this._devHosts = DEFAULT_DEV_HOSTS;
this._devHosts.push(devHosts);
}
const CMP_CHOICE_ACCEPT_ALL = 11;
const CMP_CHOICE_REJECT_ALL = 13;

this._hostname = hostname || window.location.hostname;
this.init();
}
export class ConsentMonitor {
private _consent = false;
private _devHosts: string[];
private _isDevEnvironment = false;
private _isInitialized = false;
private _hostname: string;

get consent(): boolean {
public get consent(): boolean {
return this._consent;
}

get devHosts(): string[] | string {
public get devHosts(): string[] {
return this._devHosts;
}

get isDevEnvironment(): boolean {
public get isDevEnvironment(): boolean {
return this._isDevEnvironment;
}

get isInitialized(): boolean {
public get isInitialized(): boolean {
return this._isInitialized;
}

public get userHasConsented(): boolean {
return this._consent;
}

getCookieValue = (name: string) =>
document.cookie.match("(^|;)\\s*" + name + "\\s*=\\s*([^;]+)")?.pop() || "";

init = () => {
this.cookieConsentTest();
setInterval(this.cookieConsentTest, 3000);
constructor(hostname?: string, devHosts?: string[] | string) {
if (Array.isArray(devHosts)) {
this._devHosts = [...devHosts, ...DEFAULT_DEV_HOSTS];
} else if (devHosts === undefined) {
this._devHosts = [...DEFAULT_DEV_HOSTS];
} else {
this._devHosts = [...DEFAULT_DEV_HOSTS, devHosts];
}

//Simulate cookie consent behaviour in non-prod environments
this._devHosts.map(
(devHost) =>
this._hostname.includes(devHost) && this.setDevCookieHandler(),
this._hostname = hostname || window.location.hostname;

this._isDevEnvironment = this._devHosts.some((h) =>
this._hostname.includes(h),
);
};

cookieConsentTest = () => {
if (window.permutive) {
if (!this._isInitialized) {
if (
this.getCookieValue("FTConsent").includes("behaviouraladsOnsite%3Aon")
) {
this.permutiveConsentOn();
loadFtCmpScript()
.then(() => {
this.attachCmpListeners();

const propertyConfig = window.location.hostname.endsWith(".ft.com")
? properties["FT_DOTCOM_PROD"]
: properties["FT_DOTCOM_TEST"];

// initialize CMP
initSourcepointCmp({ propertyConfig });
// use cmp client lib to intercept footer 'Manage Cookies' links (opens privacy modal)
// Note, function requires very specific link: text = 'Manage Cookies' and href = 'https://ft.com/preferences/manage-cookies'
interceptManageCookiesLinks();
this._isInitialized = true;
})
.catch((err) => console.error(err));
}

private attachCmpListeners(): void {
enqueueCmpCallback(() => {
const onReady: ConsentReadyHandler = (_l, _u, _t, info) => {
debug("onConsentReady:", info);
if (info.consentedToAll) {
this.enablePermutive();
} else {
this.permutiveConsentOff();
this.disablePermutive();
}
this._isInitialized = true;
} else if (
this.getCookieValue("FTConsent").includes(
"behaviouraladsOnsite%3Aon",
) &&
!this.consent
) {
debug("setting permutive tracking consent: on");
this.permutiveConsentOn();
} else if (
!this.getCookieValue("FTConsent").includes(
"behaviouraladsOnsite%3Aon",
) &&
this.consent
) {
debug("setting permutive tracking consent: off");
this.permutiveConsentOff();
}
}
};
};

setDevCookieHandler = () => {
this._isDevEnvironment = true;
debug("setting development environment from host match");
const oCookieMessage =
document.getElementsByClassName("o-cookie-message")[0];
if (oCookieMessage) {
const onCookieMessageAct = () => {
debug("setting development FT consent cookies");
document.cookie = "FTConsent=behaviouraladsOnsite%3Aon";
document.cookie = "FTCookieConsentGDPR=true";
oCookieMessage.removeEventListener(
"oCookieMessage.act",
onCookieMessageAct,
false,
const onChoice: MessageChoiceHandler = (_l, _c, typeId) => {
debug("onMessageChoiceSelect:", typeId);
if (typeId === CMP_CHOICE_ACCEPT_ALL) this.enablePermutive();
else if (typeId === CMP_CHOICE_REJECT_ALL) this.disablePermutive();

//Simulate cookie consent behaviour in non-prod environments as banner does not set cookies in non .ft.com domains
this._devHosts.map(
(devHost) =>
this._hostname.includes(devHost) &&
typeId === CMP_CHOICE_ACCEPT_ALL &&
this.setDevConsentCookies(),
);

// banner updated - check new cookie value to fire consent_update event
setTimeout(this.cookieConsentTest, 3000);
};
oCookieMessage.addEventListener("oCookieMessage.act", onCookieMessageAct);
}
};

permutiveConsentOn = () => {
window.permutive.consent({
window._sp_.addEventListener?.("onConsentReady", onReady);
window._sp_.addEventListener?.("onMessageChoiceSelect", onChoice);
});
}

private enablePermutive(): void {
if (this._consent) return;
debug("Permutive consent: ON");
window.permutive?.consent({
opt_in: true,
token: "behaviouraladsOnsite:on",
});
this._consent = true;
};
}

permutiveConsentOff = () => {
window.permutive.consent({ opt_in: false });
private disablePermutive(): void {
if (!this._consent) return;
debug("Permutive consent: OFF");
window.permutive?.consent({ opt_in: false });
this._consent = false;
}

//check for FTConsent - cookiesOnSite to trigger custom consent_update event for GTM tags (banner updated)
cookieConsentTest = () => {
if (!this._isInitialized || !window || !window.dataLayer) return;
if (this.getCookieValue("FTConsent").includes("cookiesOnsite%3Aon")) {
//send consent_update event
window.dataLayer.push({
event: "consent_update",
consent: true,
});
} else {
window.dataLayer.push({
event: "consent_update",
consent: false,
});
}
};

setDevConsentCookies = () => {
this._isDevEnvironment = true;
debug("setting development FT consent cookies");
document.cookie =
"FTConsent=behaviouraladsOnsite%3Aon%2CcookiesOnsite%3Aon%2CpermutiveadsOnsite%3Aon";
document.cookie = "FTCookieConsentGDPR=true";
};
}

export { ConsentMonitor as consentMonitor };
8 changes: 6 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { FTTracking, TrackingOptions } from "./FTTracking";
import { ConfigType, OrigamiEventType } from "./utils/yupValidator";

export { consentMonitor } from "./consentMonitor";
export { FTTracking };
export type { TrackingOptions };

export { loadFtCmpScript, enqueueCmpCallback } from "./cmp/loadFtCmp";

export { ConsentMonitor as consentMonitor } from "./consentMonitor";
export { permutiveVideoUtils } from "./permutiveVideoUtils";
export { reactPlayerTracking } from "./reactPlayerTracking";
export { gaTracker } from "./gaTracker";
export { oTracker } from "./oTracker";
export { ytIframeTracking } from "./ytIframeTracking";
export type { FTTracking, TrackingOptions };
export type { ConfigType, OrigamiEventType };
Loading