From f6a8fbe388775c3b784ee3fe5b64f5bd20c65971 Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Mon, 14 Feb 2022 09:44:58 -0500 Subject: [PATCH 1/9] JDB-496: first commit, copying over files from prebid-js-ftrack-module PR-1.0.0 ftrack user ID submodule code --- modules/ftrackIdSystem.js | 771 +++++++++++++++++++++++ modules/ftrackIdSystem.md | 87 +++ modules/userId/eids.js | 14 + modules/userId/eids.md | 7 + modules/userId/userId.md | 8 + test/spec/modules/ftrackIdSystem_spec.js | 221 +++++++ 6 files changed, 1108 insertions(+) create mode 100644 modules/ftrackIdSystem.js create mode 100644 modules/ftrackIdSystem.md create mode 100644 test/spec/modules/ftrackIdSystem_spec.js diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js new file mode 100644 index 00000000000..a73f47f945b --- /dev/null +++ b/modules/ftrackIdSystem.js @@ -0,0 +1,771 @@ +/** + * This module adds ftrack to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/ftrack + * @requires module:modules/userId + */ + +import * as utils from '../src/utils.js'; +import { ajax } from '../src/ajax.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { uspDataHandler } from '../src/adapterManager.js'; + +const MODULE_NAME = 'ftrackId'; +const LOG_PREFIX = 'FTRACK - '; +const LOCAL_STORAGE_EXP_DAYS = 30; +const VENDOR_ID = '000'; // TODO: how do we get a real vendor ID +const LOCAL_STORAGE = 'html5'; +const FTRACK_STORAGE_NAME = 'ftrackId'; +const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; +const storage = getStorageManager(VENDOR_ID, MODULE_NAME); + +let consentInfo = { + gdpr: { + applies: 0, + consentString: null, + pd: null + }, + usPrivacy: { + value: null + } +}; + +/** @type {Submodule} */ +export const ftrackIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: `ftrack`, + + /** + * Decodes the 'value' + * @function decode (required method) + * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config + * @returns {(Object|undefined)} an object with the key being ideally camel case + * similar to the module name and ending in id or Id + */ + decode (value, config) { + return { + ftrackId: value + }; + }, + + /** + * performs action(s) to obtain ids from D9 and return the Device IDs + * should be the only method that gets a new ID (from ajax calls or a cookie/local storage) + * @function getId (required method) + * @param {SubmoduleConfig} config + * @param {ConsentData} consentData + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + getId (config, consentData, cacheIdObj) { + if (isConfigOk(config) === false || isThereConsent(consentData) === false) return undefined; + + return { + callback: function () { + let cacheId = ''; + + let D9r = { + DeviceID: true, + SingleDeviceID: true, + callback: function(response) { + if (response) { + storage.setDataInLocalStorage(`${FTRACK_STORAGE_NAME}_exp`, (new Date(Date.now() + (1000 * 60 * 60 * 24 * LOCAL_STORAGE_EXP_DAYS))).toUTCString()); + storage.setDataInLocalStorage(`${FTRACK_STORAGE_NAME}`, JSON.stringify(response)); + + storage.setDataInLocalStorage(`${FTRACK_PRIVACY_STORAGE_NAME}_exp`, (new Date(Date.now() + (1000 * 60 * 60 * 24 * LOCAL_STORAGE_EXP_DAYS))).toUTCString()); + storage.setDataInLocalStorage(`${FTRACK_PRIVACY_STORAGE_NAME}`, JSON.stringify(consentInfo)); + }; + + return response; + } + }; + + var ftrack = { + init: val => { + ftrack.go(D9r, ftrack.collectSignals()); + }, + collectSignals: () => { + var s = {}; + var ft = ftrack.initFt(s); + var d = new Date(); + s.D9_101 = window.screen ? window.screen.width : undefined; + s.D9_102 = window.screen ? window.screen.height : undefined; + s.D9_103 = window.devicePixelRatio; + s.D9_110 = d.getTime(); + s.D9_111 = d.getTimezoneOffset(); + s.D9_120 = navigator.platform; + s.D9_121 = navigator.language || navigator.browserLanguage; + s.D9_122 = navigator.appCodeName; + s.D9_123 = navigator.maxTouchPoints || 0; + var m = ft.isM(s.D9_120, s.D9_123); + s.D9_130 = ft.flashVersion(m); + s.D9_131 = ft.acrobatVersion(m); + s.D9_132 = ft.silverlightVersion(m); + s.D9_133 = ft.getMimeTypes(m); + s.D9_134 = ft.getPlugins(m); + s.D9_140 = ft.encodeURIComponent(ft.location()); + s.D9_141 = ft.encodeURIComponent(ft.referrer()); + s.D9_150 = ft.bh64(); + s.D9_151 = ft.bh(); + return s; + }, + initFt: r => { + var ft = {}; + + function setResultObject(i, s) { + if (i !== undefined && r !== undefined) { + r['D9_'.concat(i.toString())] = s; + } + } + + var FtBh = function FtBh(options) { + var nativeForEach, nativeMap; + nativeForEach = Array.prototype.forEach; + nativeMap = Array.prototype.map; + + this.each = function (obj, iterator, context) { + if (obj === null) { + return; + } + + if (nativeForEach && obj.forEach === nativeForEach) { + obj.forEach(iterator, context); + } else { + if (obj.length === +obj.length) { + for (var i = 0, l = obj.length; i < l; i++) { + if (iterator.call(context, obj[i], i, obj) === {}) { + return; + } + } + } else { + for (var key in obj) { + if (obj.hasOwnProperty(key)) { + if (iterator.call(context, obj[key], key, obj) === {}) { + return; + } + } + } + } + } + }; + + this.map = function (obj, iterator, context) { + var results = []; + + if (obj == null) { + return results; + } + + if (nativeMap && obj.map === nativeMap) { + return obj.map(iterator, context); + } + + this.each(obj, function (value, index, list) { + results[results.length] = iterator.call(context, value, index, list); + }); + return results; + }; + + if (typeof options === 'object') { + this.hasher = options.hasher; + this.indexProperties = options.indexProperties; + } else { + if (typeof options === 'function') { + this.hasher = options; + } + } + }; + + FtBh.prototype = { + get: function get() { + var ua = navigator.userAgent.toLowerCase(); + var keys = []; + var navLang = navigator.language || navigator.browserLanguage; + var navLangArr = navLang.split('-'); + + if (typeof navLangArr[0] == 'undefined') { + // navLang = navLang; + } else { + navLang = navLangArr[0]; + } + + keys.push((this.indexProperties ? 'a:' : '') + navLang); + keys.push((this.indexProperties ? 'b:' : '') + screen.colorDepth); + keys.push((this.indexProperties ? 'c:' : '') + new Date().getTimezoneOffset()); + keys.push((this.indexProperties ? 'd:' : '') + this.hasSessionStorage()); + + if (ua.indexOf('android') == -1) { + keys.push((this.indexProperties ? 'e:' : '') + this.hasLocalStorage()); + } + + if (navigator.platform != 'iPhone' && navigator.platform != 'iPad') { + var hasDb; + + try { + hasDb = !!window.indexedDB; + } catch (e) { + hasDb = true; + } + + keys.push((this.indexProperties ? 'f:' : '') + hasDb); + } + + if (document.body) { + keys.push((this.indexProperties ? 'g:' : '') + typeof (document.body.addBehavior)); + } else { + // keys.push((this.indexProperties ? 'g:' : '') + (true ? 'undefined' : typeof (undefined))); + keys.push((this.indexProperties ? 'g:' : '') + 'undefined'); + } + + if (ua.indexOf('android') == -1) { + keys.push((this.indexProperties ? 'h:' : '') + typeof (window.openDatabase)); + } + + keys.push((this.indexProperties ? 'i:' : '') + navigator.cpuClass); + keys.push((this.indexProperties ? 'j:' : '') + navigator.platform); + + if (this.hasher) { + return this.hasher(keys.join('###'), 31); + } else { + return this.murmurhash332gc(keys.join('###'), 31); + } + }, + murmurhash332gc: function murmurhash332gc(key, seed) { + var remainder, bytes, h1, h1b, c1, c2, k1, i; + remainder = key.length & 3; + bytes = key.length - remainder; + h1 = seed; + c1 = 3432918353; + c2 = 461845907; + i = 0; + + while (i < bytes) { + k1 = key.charCodeAt(i) & 255 | (key.charCodeAt(++i) & 255) << 8 | (key.charCodeAt(++i) & 255) << 16 | (key.charCodeAt(++i) & 255) << 24; + ++i; + k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295; + k1 = k1 << 15 | k1 >>> 17; + k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295; + h1 ^= k1; + h1 = h1 << 13 | h1 >>> 19; + h1b = (h1 & 65535) * 5 + (((h1 >>> 16) * 5 & 65535) << 16) & 4294967295; + h1 = (h1b & 65535) + 27492 + (((h1b >>> 16) + 58964 & 65535) << 16); + } + + k1 = 0; + + switch (remainder) { + case 3: + k1 ^= (key.charCodeAt(i + 2) & 255) << 16; + break; + case 2: + k1 ^= (key.charCodeAt(i + 1) & 255) << 8; + break; + case 1: + k1 ^= key.charCodeAt(i) & 255; + k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295; + k1 = k1 << 15 | k1 >>> 17; + k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295; + h1 ^= k1; + break; + } + + h1 ^= key.length; + h1 ^= h1 >>> 16; + h1 = (h1 & 65535) * 2246822507 + (((h1 >>> 16) * 2246822507 & 65535) << 16) & 4294967295; + h1 ^= h1 >>> 13; + h1 = (h1 & 65535) * 3266489909 + (((h1 >>> 16) * 3266489909 & 65535) << 16) & 4294967295; + h1 ^= h1 >>> 16; + return h1 >>> 0; + }, + hasLocalStorage: function hasLocalStorage() { + try { + return !!window.localStorage; + } catch (e) { + return true; + } + }, + hasSessionStorage: function hasSessionStorage() { + try { + return !!window.sessionStorage; + } catch (e) { + return true; + } + } + }; + + ft.isM = function (p, t) { + return !!p && (p === 'iPhone' || p === 'iPad' || (p.substr(0, 7) === 'Linux a' && t > 0)); + }; + + ft.bh = function () { + return new FtBh().get(); + }; + + ft.bh64 = function () { + return new FtBh({ + indexProperties: true, + hasher: function hasher(s) { + return btoa(s); + } + }).get(); + }; + + ft.encodeURIComponent = function (value) { + if (value === undefined || value === null) { + return value; + } + + return encodeURIComponent(value); + }; + + ft.location = function () { + var l = window.location.hostname; + var rootLoc; + var rootHost; + + if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) { + rootLoc = window.location.ancestorOrigins[window.location.ancestorOrigins.length - 1]; + rootHost = ft.hostName(rootLoc); + + if (rootHost) { + l = rootHost; + } + } + + if (!l) { + l = ''; + } + + return l; + }; + + ft.referrer = function () { + var r = ft.hostName(document.referrer); + + if (r === ft.location()) { + r = ''; + } + + if (!r) { + r = ''; + } + + return r; + }; + + ft.hostName = function (urlString) { + try { + var url = new URL(urlString); + return url.hostname; + } catch (e) {} + }; + + ft.flashVersion = function (m) { + setResultObject(138, cacheId); + + if (m) { + return null; + } + + try { + try { + var obj = new window.ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); + + try { + obj.AllowScriptAccess = 'always'; + } catch (e) { + return '6.0.0'; + } + } catch (e) {} + + return new window.ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, '.').match(/^.?(.+),?$/)[1]; + } catch (e) { + try { + if (navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin) { + return (navigator.plugins['Shockwave Flash 2.0'] || navigator.plugins['Shockwave Flash']).description.replace(/\D+/g, '.').match(/^.?(.+),?$/)[1]; + } + } catch (e) {} + } + + return null; + }; + + ft.acrobatVersion = function (m) { + setResultObject(139, 'd10e54cb41fb43c5945b4d01b9bde1da'); + + if (m) { + return null; + } + + if (window.hasOwnProperty('ActiveXObject')) { + var obj = null; + + try { + obj = new window.ActiveXObject('AcroPDF.PDF'); + } catch (e) {} + + if (!obj) { + try { + obj = new window.ActiveXObject('PDF.PdfCtrl'); + } catch (e) { + return null; + } + } + + if (obj) { + var version = obj.GetVersions().split(','); + version = version[0].split('='); + version = parseFloat(version[1]); + return version; + } else { + return null; + } + } else { + for (var i = 0; i < navigator.plugins.length; i++) { + if (navigator.plugins[i].name.indexOf('Adobe Acrobat') != -1) { + return navigator.plugins[i].description.replace(/\D+/g, '.').match(/^.?(.+),?$/)[1]; + } + } + + return null; + } + }; + + ft.silverlightVersion = function (m) { + var i; + if (m) { + return null; + } + + var parts = ['', '', '', '']; + var nav = navigator.plugins['Silverlight Plug-In']; + + if (nav) { + for (i = 0; i < 4; i++) { + parts[i] = parseInt(nav.description.split('.')[i]).toString(); + } + } else { + try { + var control = new window.ActiveXObject('AgControl.AgControl'); + var vers = [1, 0, 0, 0]; + loopMatch(control, vers, 0, 1); + loopMatch(control, vers, 1, 1); + loopMatch(control, vers, 2, 10000); + loopMatch(control, vers, 2, 1000); + loopMatch(control, vers, 2, 100); + loopMatch(control, vers, 2, 10); + loopMatch(control, vers, 2, 1); + loopMatch(control, vers, 3, 1); + + for (i = 0; i < 4; i++) { + parts[i] = vers[i].toString(); + } + } catch (e) { + return null; + } + } + + return parts.join('.'); + + function loopMatch(control, vers, idx, inc) { + while (IsSupported(control, vers)) { + vers[idx] += inc; + } + + vers[idx] -= inc; + } + + function IsSupported(control, ver) { + return control.isVersionSupported(ver[0] + '.' + ver[1] + '.' + ver[2] + '.' + ver[3]); + } + }; + + ft.getPlugins = function (m) { + var a = []; + + if (m) { + return a; + } + + try { + for (var i = 0; i < navigator.plugins.length; i++) { + a.push(navigator.plugins[i].name + ': ' + navigator.plugins[i].description + ' (' + navigator.plugins[i].filename + ')'); + } + + return a; + } catch (e) { + return null; + } + }; + + ft.getMimeTypes = function (m) { + var a = []; + + if (m) { + return a; + } + + try { + for (var i = 0; i < navigator.mimeTypes.length; i++) { + a.push(navigator.mimeTypes[i].type + ': ' + navigator.mimeTypes[i].description); + } + + return a; + } catch (e) { + return null; + } + }; + + return ft; + }, + go: (D9r, signals) => { + let tagHost = 'd9.flashtalking.com'; + + function D9request(D9Device) { + var json = encodeURIComponent(JSON.stringify(D9Device)); + var send = '&tbx=' + encodeURIComponent(json); + ajax(getLgcUrl(), send); + } + + function getLgcUrl() { + var httpProto = 'https://'; + var lgcUrl = tagHost + '/lgc'; + return httpProto + lgcUrl; + } + + function getConnectUrl() { + var httpProto = 'https://'; + var lgcUrl = tagHost + '/img/img.png?cnx=' + (device && device.D9_61 ? device.D9_61 : ''); + return httpProto + lgcUrl; + } + + function ajax(url, send) { + if (window.d9PendingXDR != undefined) { + return; + } + + var ar = null; + + function corsSupported() { + try { + return typeof XMLHttpRequest !== 'undefined' && 'withCredentials' in new XMLHttpRequest(); + } catch (e) { + return false; + } + } + + if (typeof window.XDomainRequest !== 'undefined' && !corsSupported()) { + ar = new XDomainRequest(); + ar.open('POST', url); + } else { + try { + ar = new XMLHttpRequest(); + } catch (e) { + if (window.hasOwnProperty('ActiveXObject')) { + var ax = ['Msxml2.XMLHTTP.3.0', 'Msxml2.XMLHTTP.4.0', 'Msxml2.XMLHTTP.6.0', 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP']; + var i = ax.length; + + while (--i) { + try { + ar = new window.ActiveXObject(ax[i]); + break; + } catch (e) {} + } + } + } + + ar.open('POST', url, true); + ar.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); + ar.withCredentials = true; + } + + ar.onreadystatechange = function () { + if (D9r && D9r.L > 0) { + return; + } + + if (D9r && D9r.callback && ar.readyState == 4) { + if (ar.status == 200 || ar.status == 304) { + var r = ar.responseText; + var response; + + if (r && r.length > 0) { + try { + response = JSON.parse(r); + } catch (e) {} + } + + if (response) { + if (response.cnx) { + new Image().src = getConnectUrl(); + delete response.cnx; + } + + D9r.callback(response); + } else { + D9r.callback(null); + } + } + } + }; + + if (typeof window.XDomainRequest !== 'undefined' && !corsSupported()) { + ar.ontimeout = ar.onerror = function (e) { + ar.status = 400; + ar.readyState = 4; + ar.onreadystatechange(); + }; + + ar.onload = function () { + ar.status = 200; + ar.readyState = 4; + ar.onreadystatechange(); + }; + + ar.onprogress = function () {}; + } + + ar.send(send); + window.d9PendingXDR = ar; + }; + + var device = {}; + device.D9_1 = signals.D9_110; + device.D9_6 = signals.D9_130; + device.D9_7 = signals.D9_131; + device.D9_8 = signals.D9_132; + device.D9_9 = signals.D9_133; + device.D9_10 = signals.D9_134; + device.D9_61 = signals.D9_138; + device.D9_67 = signals.D9_139; + device.D9_18 = {}; + device.D9_16 = signals.D9_111; + + if (signals.D9_101 || signals.D9_111) { + device.D9_4 = { + width: signals.D9_101, + height: signals.D9_102 + }; + } + + if (window.navigator) { + device.D9_14 = signals.D9_120; + device.D9_15 = signals.D9_121; + device.D9_19 = signals.D9_122; + } + + device.D9_33 = signals.D9_150; + device.D9_34 = signals.D9_151; + device.D9_30 = []; + device.D9_52 = {}; + device.D9_57 = typeof D9r.callback === 'function'; + device.D9_58 = D9r; + device.D9_59 = { CampID: 3175, CCampID: 156515 }; + device.D9_63 = signals.D9_140; + device.D9_64 = signals.D9_103; + device.D9_66 = signals.D9_141; + D9request(device); + } + }; + + ajax( + 'https://e.flashtalking.com/cache', + { + success: response => { + cacheId = JSON.parse(response).cache_id || ''; + ftrack.init(cacheId); + }, + error: error => { + ftrack.init(error); + } + } + ); + } + }; + }, + + /** + * Called when IDs are already in localStorage + * should just be adding additional data to the cacheIdObj object + * @function extendId (optional method) + * @param {SubmoduleConfig} config + * @param {ConsentData} consentData + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + extendId (config, consentData, cacheIdObj) { + isConfigOk(config); + return cacheIdObj; + } +}; + +/* + * Validates the config, if it is not correct, then info cannot be saved in localstorage + * @function isConfigOk + * @param {SubmoduleConfig} config from HTML + * @returns {true|false} + */ +const isConfigOk = function(config) { + if (!config.storage || !config.storage.type || !config.storage.name) { + utils.logError(LOG_PREFIX + 'storage required to be set'); + return false; + } + + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.type !== LOCAL_STORAGE) { + utils.logWarn(LOG_PREFIX + `storage type recommended to be '${LOCAL_STORAGE}'.`); + } + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.name !== FTRACK_STORAGE_NAME) { + utils.logWarn(LOG_PREFIX + `storage name recommended to be '${FTRACK_STORAGE_NAME}'.`); + } + + return true; +}; + +const isThereConsent = function(consentData) { + let consentValue = true; + + /* + * Scenario 1: GDPR + * if GDPR Applies is true|1, we do not have consent + * if GDPR Applies does not exist or is false|0, we do not NOT have consent + */ + if (consentData && consentData.gdprApplies && (consentData.gdprApplies === true || consentData.gdprApplies === 1)) { + consentInfo.gdpr.applies = 1; + consentValue = false; + } + // If consentString exists, then we store it even though we are not using it + if (consentData && consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) { + consentInfo.gdpr.consentString = consentData.consentString; + } + + /* + * Scenario 2: CCPA/us_privacy + * if usp exists (assuming this check determines the location of the device to be within the California) + * parse the us_privacy string to see if we have consent + * for version 1 of us_privacy strings, if 'Opt-Out Sale' is 'Y' we do not track + */ + const usp = uspDataHandler.getConsentData(); + let usPrivacyVersion; + // let usPrivacyOptOut; + let usPrivacyOptOutSale; + // let usPrivacyLSPA; + if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) { + consentInfo.usPrivacy.value = usp; + usPrivacyVersion = usp[0]; + // usPrivacyOptOut = usp[1]; + usPrivacyOptOutSale = usp[2]; + // usPrivacyLSPA = usp[3]; + } + if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false; + + return consentValue; +}; + +submodule('userId', ftrackIdSubmodule); diff --git a/modules/ftrackIdSystem.md b/modules/ftrackIdSystem.md new file mode 100644 index 00000000000..983262aed3d --- /dev/null +++ b/modules/ftrackIdSystem.md @@ -0,0 +1,87 @@ +# Flashtalking's FTrack Identity Framework User ID Module + +*The FTrack Identity Framework User ID Module allows publishers to take advantage of Flashtalking's FTrack ID during the bidding process.* + +**THE ONLY COMPLETE SOLUTION FOR COOKIELESS MEASUREMENT & PERSONALIZATION** + +Flashtalking is the world’s first ad serving platform to function without cookies to orchestrate client identity across buy-side ID spaces for measurement and personalization. With over 120 active global advertisers, our cookieless identity framework is market-ready and includes privacy controls to ensure consumer notification and choice on every impression. + +### [FTrack](https://www.flashtalking.com/identity-framework#FTrack) + +Flashtalking’s cookieless tracking technology uses probabilistic device recognition to derive a privacy-friendly persistent ID for each device. + +**PROVEN** +With over 120 brands using FTrack globally, Flashtalking has accumulated the largest cookieless footprint in the industry. + +**ANTI-FINGERPRINTING** +FTrack operates in strict compliance with [Google’s definition of anti-fingerprinting](https://blog.google/products/ads-commerce/2021-01-privacy-sandbox/). FTrack does not access PII or sensitive information and provides consumers with notification and choice on every impression. We do not participate in the types of activities that most concern privacy advocates (profiling consumers, building audience segments, and/or monetizing consumer data). + +**GDPR COMPLIANT** +Flashtalking is integrated with the IAB EU’s Transparency & Consent Framework (TCF) and operates on a Consent legal basis where required. As a Data Processor under GDPR, Flashtalking does not combine data across customers nor sell data to third parties. + +**ACCURATE** +FTrack’s broad adoption combined with the maturity of the models (6+ years old) gives Flashtalking the global scale with which to maintain a high degree of model resolution and accuracy. + +**DURABLE** +As new IDs start to proliferate, they will serve as new incremental signals for our models. + +--- + +### Support or Maintenance: + +Questions? Comments? Bugs? Praise? Please contact FlashTalking's Prebid Support at [prebid-support@flashtalking.com](mailto:prebid-support@flashtalking.com) + +--- + +### FTrack User ID Configuration + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'FTrack', + storage: { + type: 'html5', // "html5" is the required storage type + name: 'FTrackId', // "FTrackId" is the required storage name + expires: 90, // storage lasts for 90 days + refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh + } + }], + auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules + } +}); +``` + + + + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +| :-- | :-- | :-- | :-- | :-- | +| name | Required | String | The name of this module: `"FTrack"` | `"FTrack"` | +| storage | Required | Object | Storage settings for how the User ID module will cache the FTrack ID locally | | +| storage.type | Required | String | This is where the results of the user ID will be stored. FTrack **requires** `"html5"`. | `"html5"` | +| storage.name | Required | String | The name of the local storage where the user ID will be stored. FTrack **requires** `"FTrackId"`. | `"FTrackId"` | +| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. FTrack recommends `90`. | `90` | +| storage.refreshInSeconds | Optional | Integer | How many seconds until the FTrack ID will be refreshed. FTrack strongly recommends 8 hours between refreshes | `8*3600` | + +**ATTENTION:** As of Prebid.js v4.14.0, FTrack requires `storage.type` to be `"html5"` and `storage.name` to be `"FTrackId"`. Using other values will display a warning today, but in an upcoming release, it will prevent the FTrack module from loading. This change is to ensure the FTrack module in Prebid.js interoperates properly with the [FTrack](https://www.flashtalking.com/identity-framework#FTrack) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid-support@flashtalking.com](mailto:prebid-support@flashtalking.com). + +--- + +### Privacy Policies. + +Complete information available on the Flashtalking [privacy policy page](https://www.flashtalking.com/privacypolicy). + +#### OPTING OUT OF INTEREST-BASED ADVERTISING & COLLECTION OF PERSONAL INFORMATION + +Please visit our [Opt Out Page](https://www.flashtalking.com/optout). + +#### REQUEST REMOVAL OF YOUR PERSONAL DATA (WHERE APPLICABLE) + +You may request by emailing [mailto:privacy@flashtalking.com](privacy@flashtalking.com). + +#### GDPR + +In its current state, Flashtalking’s FTrack Identity Framework User ID Module does not create an ID if a user's consentData is "truthy" (true, 1). In other words, if GDPR applies in any way to a user, FTrack does not create an ID. \ No newline at end of file diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 87b6ecc1f1c..05d42537a3b 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -48,6 +48,20 @@ const USER_IDS_CONFIG = { } }, + // ftrack + 'ftrackId': { + source: 'flashtalking.com', + atype: 1, + getValue: function(data) { + return data.uid + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext; + } + } + }, + // parrableId 'parrableId': { source: 'parrable.com', diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 679bf5ffe27..29f7d8c7064 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -49,6 +49,13 @@ userIdAsEids = [ }] }, + { + source: 'ftrack.com', + uids: [{ + id: 'some-random-id-value', + atype: 1 + }, + { source: 'parrable.com', uids: [{ diff --git a/modules/userId/userId.md b/modules/userId/userId.md index 095685aba3d..c582262b5f2 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -44,6 +44,14 @@ pbjs.setConfig({ expires: 90, // Expiration in days refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires' }, + }, { + name: "ftrackId", + storage: { + type: "html5", + name: "ftrackId", + expires: 90, + refreshInSeconds: 8*3600 + }, }, { name: 'parrableId', params: { diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js new file mode 100644 index 00000000000..422b3842d6b --- /dev/null +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -0,0 +1,221 @@ +import { ftrackIdSubmodule } from 'modules/ftrackIdSystem.js'; +import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; +import * as utils from 'src/utils.js'; +import { uspDataHandler } from 'src/adapterManager.js'; +let expect = require('chai').expect; + +let server, requests; + +let configMock = { + name: 'ftrack', + storage: { + name: 'ftrackId', + type: 'html5', + expires: 90, + refreshInSeconds: 8 * 3600 + }, + debug: true +}; + +let consentDataMock = { + gdprApplies: 0, + consentString: '' +}; + +describe('FTRACK ID System', () => { + describe(`Global Module Rules`, () => { + it(`should not use the "PREBID_GLOBAL" variable nor otherwise obtain a pointer to the global PBJS object`, () => { + expect((/PREBID_GLOBAL/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); + }); + + describe('Publisher config:', () => { + let logWarnStub; + let logErrorStub; + beforeEach(() => { + logWarnStub = sinon.stub(utils, 'logWarn'); + logErrorStub = sinon.stub(utils, 'logError'); + }); + afterEach(() => { + logWarnStub.restore(); + logErrorStub.restore(); + }); + + it(`should be rejected if 'storage' property is missing`, () => { + expect(ftrackIdSubmodule.getId({name: 'ftrack'})).to.be.undefined; + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - storage required to be set`); + }); + + it(`should be rejected if 'storage.type' property is missing`, () => { + expect(ftrackIdSubmodule.getId({ + name: 'ftrack', + storage: { + type: 'html5', + expires: 90, + refreshInSeconds: 8 * 3600 + } + }, null, null)).to.be.undefined; + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - storage required to be set`); + }); + + it(`should be rejected if 'storage.name' property is missing`, () => { + expect(ftrackIdSubmodule.getId({ + name: 'ftrack', + storage: { + name: 'ftrackId', + expires: 90, + refreshInSeconds: 8 * 3600 + } + }, null, null)).to.be.undefined; + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - storage required to be set`); + }); + + it(`should be rejected if 'storage.name' is not 'ftrackId'`, () => { + expect(ftrackIdSubmodule.getId({ + name: 'ftrack', + storage: { + name: 'lorem ipsum', + type: 'html5', + expires: 90, + refreshInSeconds: 8 * 3600 + } + }, null, null).callback()).to.equal(undefined); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - storage name recommended to be 'ftrackId'.`); + }); + + it(`should be rejected if 'storage.type' is not 'html5'`, () => { + expect(ftrackIdSubmodule.getId({ + name: 'ftrack', + storage: { + name: 'ftrackId', + type: 'cookies', + expires: 90, + refreshInSeconds: 8 * 3600 + } + }, null, null).callback()).to.equal(undefined); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - storage type recommended to be 'html5'.`); + }); + }); + + describe('getId() method', () => { + it(`should be using the StorageManager to set cookies or localstorage, as opposed to doing it directly`, () => { + expect((/localStorage/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + expect((/cookie/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); + + describe(`endpoint tests - `, () => { + beforeEach(() => { + server = sinon.createFakeServer(); + }); + + afterEach(() => { + server.restore(); + }); + + it(`should request the cacheId from the '/cache' endpoint`, () => { + ftrackIdSubmodule.getId(configMock, null, null).callback(); + expect((/.flashtalking\.com\/cache/).test(server.requests[0].url)).to.be.ok; + }); + + it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => { + ftrackIdSubmodule.getId(configMock, null, null).callback(); + expect(server.requests).to.have.length(1); + server.resetHistory(); + + ftrackIdSubmodule.decode('value', configMock); + expect(server.requests).to.have.length(0); + server.resetHistory(); + + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + expect(server.requests).to.have.length(0); + }); + + it(`should populate localstorage (end-to-end test)`, () => { + let lgcResponseMock = { + 'DeviceID': [''], + 'SingleDeviceID': [''] + }; + ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); + expect((/.flashtalking\.com\/cache/).test(server.requests[0].url)).to.be.ok; + server.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{"cache_id":""}'); + expect((/lgc/).test(server.requests[1].url)).to.be.ok; + server.requests[1].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(lgcResponseMock)); + + expect(localStorage.getItem('ftrackId')).to.equal(JSON.stringify(lgcResponseMock)); + expect(localStorage.getItem('ftrackId_exp')).to.be.ok; + expect(localStorage.getItem('ftrackId_privacy')).to.equal(JSON.stringify({'gdpr': {'applies': 0, 'consentString': '', 'pd': null}, 'usPrivacy': {'value': null}})); + expect(localStorage.getItem('ftrackId_privacy_exp')).to.be.ok; + }); + }); + + describe(`consent options - `, () => { + let uspDataHandlerStub; + beforeEach(() => { + uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + }); + + afterEach(() => { + uspDataHandlerStub.restore(); + }); + + describe(`getId() should return undefined`, () => { + it(`GDPR: if gdprApplies is truthy`, () => { + expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: 1}, null)).to.not.be.ok; + expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: true}, null)).to.not.be.ok; + }); + + it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => { + uspDataHandlerStub.returns('1YYY'); + expect(ftrackIdSubmodule.getId(configMock, {}, null)).to.not.be.ok; + }); + }); + + describe(`getId() should run`, () => { + it(`GDPR: if gdprApplies is undefined, false or 0`, () => { + expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: 0}, null)).to.be.ok; + expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: false}, null)).to.be.ok; + expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: null}, null)).to.be.ok; + expect(ftrackIdSubmodule.getId(configMock, {}, null)).to.be.ok; + }); + + it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => { + uspDataHandlerStub.returns('1NNN'); + expect(ftrackIdSubmodule.getId(configMock, null, null)).to.be.ok; + + uspDataHandlerStub.returns('1---'); + expect(ftrackIdSubmodule.getId(configMock, null, null)).to.be.ok; + }); + }); + }); + }); + + describe(`decode() method`, () => { + it(`should respond with an object with the key 'ftrackId'`, () => { + expect(ftrackIdSubmodule.decode('value', configMock)).to.deep.equal({ftrackId: 'value'}); + }); + + it(`should not be making requests to retrieve a new ID, it should just be decoding a response`, () => { + server = sinon.createFakeServer(); + ftrackIdSubmodule.decode('value', configMock); + + expect(server.requests).to.have.length(0); + + server.restore(); + }) + }); + + describe(`extendId() method`, () => { + it(`should not be making requests to retrieve a new ID, it should just be adding additional data to the id object`, () => { + server = sinon.createFakeServer(); + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + + expect(server.requests).to.have.length(0); + + server.restore(); + }); + + it(`should return cacheIdObj`, () => { + expect(ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}})).to.deep.equal({cache: {id: ''}}); + }); + }); +}); From 19ccd2b306e6b1e183c7cf363cb695ea26164d7d Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Mon, 14 Feb 2022 10:25:25 -0500 Subject: [PATCH 2/9] Addressing the lgtm alert --- modules/ftrackIdSystem.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index a73f47f945b..f7838ae38e9 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -407,17 +407,19 @@ export const ftrackIdSubmodule = { try { obj = new window.ActiveXObject('AcroPDF.PDF'); - } catch (e) {} + } catch (e) { + obj = null; + } if (!obj) { try { obj = new window.ActiveXObject('PDF.PdfCtrl'); } catch (e) { - return null; + obj = null; } } - if (obj) { + if (obj !== null) { var version = obj.GetVersions().split(','); version = version[0].split('='); version = parseFloat(version[1]); From 5da70974a2ba870da29e4cebfa866d1c8ff9f89d Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Mon, 14 Feb 2022 11:32:28 -0500 Subject: [PATCH 3/9] Addressing the remaining lgtm alerts --- test/spec/modules/ftrackIdSystem_spec.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js index 422b3842d6b..b9d3099386a 100644 --- a/test/spec/modules/ftrackIdSystem_spec.js +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -1,10 +1,9 @@ import { ftrackIdSubmodule } from 'modules/ftrackIdSystem.js'; -import { init, requestBidsHook, setSubmoduleRegistry, coreStorage } from 'modules/userId/index.js'; import * as utils from 'src/utils.js'; import { uspDataHandler } from 'src/adapterManager.js'; let expect = require('chai').expect; -let server, requests; +let server; let configMock = { name: 'ftrack', @@ -104,6 +103,7 @@ describe('FTRACK ID System', () => { }); describe(`endpoint tests - `, () => { + let cacheUrlRegExp = /https:\/\/e\.flashtalking\.com\/cache/; beforeEach(() => { server = sinon.createFakeServer(); }); @@ -114,7 +114,7 @@ describe('FTRACK ID System', () => { it(`should request the cacheId from the '/cache' endpoint`, () => { ftrackIdSubmodule.getId(configMock, null, null).callback(); - expect((/.flashtalking\.com\/cache/).test(server.requests[0].url)).to.be.ok; + expect((cacheUrlRegExp).test(server.requests[0].url)).to.be.ok; }); it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => { @@ -136,7 +136,7 @@ describe('FTRACK ID System', () => { 'SingleDeviceID': [''] }; ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); - expect((/.flashtalking\.com\/cache/).test(server.requests[0].url)).to.be.ok; + expect((cacheUrlRegExp).test(server.requests[0].url)).to.be.ok; server.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{"cache_id":""}'); expect((/lgc/).test(server.requests[1].url)).to.be.ok; server.requests[1].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(lgcResponseMock)); From 4ba62080fe7a47847ac14dfd87d009af46f6038f Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Wed, 16 Feb 2022 11:01:42 -0500 Subject: [PATCH 4/9] Setting VENDOR_ID to null for now --- modules/ftrackIdSystem.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index f7838ae38e9..7fef9b3b074 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -14,7 +14,7 @@ import { uspDataHandler } from '../src/adapterManager.js'; const MODULE_NAME = 'ftrackId'; const LOG_PREFIX = 'FTRACK - '; const LOCAL_STORAGE_EXP_DAYS = 30; -const VENDOR_ID = '000'; // TODO: how do we get a real vendor ID +const VENDOR_ID = null; const LOCAL_STORAGE = 'html5'; const FTRACK_STORAGE_NAME = 'ftrackId'; const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; From 8daacfe53cf1542ffdf620c2787a6a0025e8525c Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Thu, 24 Feb 2022 15:57:04 -0500 Subject: [PATCH 5/9] Pulled ftrack out and used a config.param property instead to load ftrack --- modules/.submodules.json | 1 + modules/ftrackIdSystem.js | 747 +++-------------------- modules/ftrackIdSystem.md | 3 + modules/userId/userId.md | 3 + test/spec/modules/ftrackIdSystem_spec.js | 269 ++++---- 5 files changed, 229 insertions(+), 794 deletions(-) diff --git a/modules/.submodules.json b/modules/.submodules.json index ea3f556dbb4..099ee43f160 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -13,6 +13,7 @@ "flocIdSystem", "haloIdSystem", "id5IdSystem", + "ftrackIdSystem", "identityLinkIdSystem", "idxIdSystem", "imuIdSystem", diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index 7fef9b3b074..a057f0d2474 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -6,7 +6,6 @@ */ import * as utils from '../src/utils.js'; -import { ajax } from '../src/ajax.js'; import { submodule } from '../src/hook.js'; import { getStorageManager } from '../src/storageManager.js'; import { uspDataHandler } from '../src/adapterManager.js'; @@ -18,6 +17,7 @@ const VENDOR_ID = null; const LOCAL_STORAGE = 'html5'; const FTRACK_STORAGE_NAME = 'ftrackId'; const FTRACK_PRIVACY_STORAGE_NAME = `${FTRACK_STORAGE_NAME}_privacy`; +const FTRACK_URL = 'https://d9.flashtalking.com/d9core'; const storage = getStorageManager(VENDOR_ID, MODULE_NAME); let consentInfo = { @@ -63,13 +63,16 @@ export const ftrackIdSubmodule = { * @returns {IdResponse|undefined} */ getId (config, consentData, cacheIdObj) { - if (isConfigOk(config) === false || isThereConsent(consentData) === false) return undefined; + if (this.isConfigOk(config) === false || this.isThereConsent(consentData) === false) return undefined; return { callback: function () { - let cacheId = ''; - - let D9r = { + window.D9v = { + UserID: '99999999999999', + CampID: '3175', + CCampID: '148556' + }; + window.D9r = { DeviceID: true, SingleDeviceID: true, callback: function(response) { @@ -85,608 +88,11 @@ export const ftrackIdSubmodule = { } }; - var ftrack = { - init: val => { - ftrack.go(D9r, ftrack.collectSignals()); - }, - collectSignals: () => { - var s = {}; - var ft = ftrack.initFt(s); - var d = new Date(); - s.D9_101 = window.screen ? window.screen.width : undefined; - s.D9_102 = window.screen ? window.screen.height : undefined; - s.D9_103 = window.devicePixelRatio; - s.D9_110 = d.getTime(); - s.D9_111 = d.getTimezoneOffset(); - s.D9_120 = navigator.platform; - s.D9_121 = navigator.language || navigator.browserLanguage; - s.D9_122 = navigator.appCodeName; - s.D9_123 = navigator.maxTouchPoints || 0; - var m = ft.isM(s.D9_120, s.D9_123); - s.D9_130 = ft.flashVersion(m); - s.D9_131 = ft.acrobatVersion(m); - s.D9_132 = ft.silverlightVersion(m); - s.D9_133 = ft.getMimeTypes(m); - s.D9_134 = ft.getPlugins(m); - s.D9_140 = ft.encodeURIComponent(ft.location()); - s.D9_141 = ft.encodeURIComponent(ft.referrer()); - s.D9_150 = ft.bh64(); - s.D9_151 = ft.bh(); - return s; - }, - initFt: r => { - var ft = {}; - - function setResultObject(i, s) { - if (i !== undefined && r !== undefined) { - r['D9_'.concat(i.toString())] = s; - } - } - - var FtBh = function FtBh(options) { - var nativeForEach, nativeMap; - nativeForEach = Array.prototype.forEach; - nativeMap = Array.prototype.map; - - this.each = function (obj, iterator, context) { - if (obj === null) { - return; - } - - if (nativeForEach && obj.forEach === nativeForEach) { - obj.forEach(iterator, context); - } else { - if (obj.length === +obj.length) { - for (var i = 0, l = obj.length; i < l; i++) { - if (iterator.call(context, obj[i], i, obj) === {}) { - return; - } - } - } else { - for (var key in obj) { - if (obj.hasOwnProperty(key)) { - if (iterator.call(context, obj[key], key, obj) === {}) { - return; - } - } - } - } - } - }; - - this.map = function (obj, iterator, context) { - var results = []; - - if (obj == null) { - return results; - } - - if (nativeMap && obj.map === nativeMap) { - return obj.map(iterator, context); - } - - this.each(obj, function (value, index, list) { - results[results.length] = iterator.call(context, value, index, list); - }); - return results; - }; - - if (typeof options === 'object') { - this.hasher = options.hasher; - this.indexProperties = options.indexProperties; - } else { - if (typeof options === 'function') { - this.hasher = options; - } - } - }; - - FtBh.prototype = { - get: function get() { - var ua = navigator.userAgent.toLowerCase(); - var keys = []; - var navLang = navigator.language || navigator.browserLanguage; - var navLangArr = navLang.split('-'); - - if (typeof navLangArr[0] == 'undefined') { - // navLang = navLang; - } else { - navLang = navLangArr[0]; - } - - keys.push((this.indexProperties ? 'a:' : '') + navLang); - keys.push((this.indexProperties ? 'b:' : '') + screen.colorDepth); - keys.push((this.indexProperties ? 'c:' : '') + new Date().getTimezoneOffset()); - keys.push((this.indexProperties ? 'd:' : '') + this.hasSessionStorage()); - - if (ua.indexOf('android') == -1) { - keys.push((this.indexProperties ? 'e:' : '') + this.hasLocalStorage()); - } - - if (navigator.platform != 'iPhone' && navigator.platform != 'iPad') { - var hasDb; - - try { - hasDb = !!window.indexedDB; - } catch (e) { - hasDb = true; - } - - keys.push((this.indexProperties ? 'f:' : '') + hasDb); - } - - if (document.body) { - keys.push((this.indexProperties ? 'g:' : '') + typeof (document.body.addBehavior)); - } else { - // keys.push((this.indexProperties ? 'g:' : '') + (true ? 'undefined' : typeof (undefined))); - keys.push((this.indexProperties ? 'g:' : '') + 'undefined'); - } - - if (ua.indexOf('android') == -1) { - keys.push((this.indexProperties ? 'h:' : '') + typeof (window.openDatabase)); - } - - keys.push((this.indexProperties ? 'i:' : '') + navigator.cpuClass); - keys.push((this.indexProperties ? 'j:' : '') + navigator.platform); - - if (this.hasher) { - return this.hasher(keys.join('###'), 31); - } else { - return this.murmurhash332gc(keys.join('###'), 31); - } - }, - murmurhash332gc: function murmurhash332gc(key, seed) { - var remainder, bytes, h1, h1b, c1, c2, k1, i; - remainder = key.length & 3; - bytes = key.length - remainder; - h1 = seed; - c1 = 3432918353; - c2 = 461845907; - i = 0; - - while (i < bytes) { - k1 = key.charCodeAt(i) & 255 | (key.charCodeAt(++i) & 255) << 8 | (key.charCodeAt(++i) & 255) << 16 | (key.charCodeAt(++i) & 255) << 24; - ++i; - k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295; - k1 = k1 << 15 | k1 >>> 17; - k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295; - h1 ^= k1; - h1 = h1 << 13 | h1 >>> 19; - h1b = (h1 & 65535) * 5 + (((h1 >>> 16) * 5 & 65535) << 16) & 4294967295; - h1 = (h1b & 65535) + 27492 + (((h1b >>> 16) + 58964 & 65535) << 16); - } - - k1 = 0; - - switch (remainder) { - case 3: - k1 ^= (key.charCodeAt(i + 2) & 255) << 16; - break; - case 2: - k1 ^= (key.charCodeAt(i + 1) & 255) << 8; - break; - case 1: - k1 ^= key.charCodeAt(i) & 255; - k1 = (k1 & 65535) * c1 + (((k1 >>> 16) * c1 & 65535) << 16) & 4294967295; - k1 = k1 << 15 | k1 >>> 17; - k1 = (k1 & 65535) * c2 + (((k1 >>> 16) * c2 & 65535) << 16) & 4294967295; - h1 ^= k1; - break; - } - - h1 ^= key.length; - h1 ^= h1 >>> 16; - h1 = (h1 & 65535) * 2246822507 + (((h1 >>> 16) * 2246822507 & 65535) << 16) & 4294967295; - h1 ^= h1 >>> 13; - h1 = (h1 & 65535) * 3266489909 + (((h1 >>> 16) * 3266489909 & 65535) << 16) & 4294967295; - h1 ^= h1 >>> 16; - return h1 >>> 0; - }, - hasLocalStorage: function hasLocalStorage() { - try { - return !!window.localStorage; - } catch (e) { - return true; - } - }, - hasSessionStorage: function hasSessionStorage() { - try { - return !!window.sessionStorage; - } catch (e) { - return true; - } - } - }; - - ft.isM = function (p, t) { - return !!p && (p === 'iPhone' || p === 'iPad' || (p.substr(0, 7) === 'Linux a' && t > 0)); - }; - - ft.bh = function () { - return new FtBh().get(); - }; - - ft.bh64 = function () { - return new FtBh({ - indexProperties: true, - hasher: function hasher(s) { - return btoa(s); - } - }).get(); - }; - - ft.encodeURIComponent = function (value) { - if (value === undefined || value === null) { - return value; - } - - return encodeURIComponent(value); - }; - - ft.location = function () { - var l = window.location.hostname; - var rootLoc; - var rootHost; - - if (window.location.ancestorOrigins && window.location.ancestorOrigins.length > 0) { - rootLoc = window.location.ancestorOrigins[window.location.ancestorOrigins.length - 1]; - rootHost = ft.hostName(rootLoc); - - if (rootHost) { - l = rootHost; - } - } - - if (!l) { - l = ''; - } - - return l; - }; - - ft.referrer = function () { - var r = ft.hostName(document.referrer); - - if (r === ft.location()) { - r = ''; - } - - if (!r) { - r = ''; - } - - return r; - }; - - ft.hostName = function (urlString) { - try { - var url = new URL(urlString); - return url.hostname; - } catch (e) {} - }; - - ft.flashVersion = function (m) { - setResultObject(138, cacheId); - - if (m) { - return null; - } - - try { - try { - var obj = new window.ActiveXObject('ShockwaveFlash.ShockwaveFlash.6'); - - try { - obj.AllowScriptAccess = 'always'; - } catch (e) { - return '6.0.0'; - } - } catch (e) {} - - return new window.ActiveXObject('ShockwaveFlash.ShockwaveFlash').GetVariable('$version').replace(/\D+/g, '.').match(/^.?(.+),?$/)[1]; - } catch (e) { - try { - if (navigator.mimeTypes['application/x-shockwave-flash'].enabledPlugin) { - return (navigator.plugins['Shockwave Flash 2.0'] || navigator.plugins['Shockwave Flash']).description.replace(/\D+/g, '.').match(/^.?(.+),?$/)[1]; - } - } catch (e) {} - } - - return null; - }; - - ft.acrobatVersion = function (m) { - setResultObject(139, 'd10e54cb41fb43c5945b4d01b9bde1da'); - - if (m) { - return null; - } - - if (window.hasOwnProperty('ActiveXObject')) { - var obj = null; - - try { - obj = new window.ActiveXObject('AcroPDF.PDF'); - } catch (e) { - obj = null; - } - - if (!obj) { - try { - obj = new window.ActiveXObject('PDF.PdfCtrl'); - } catch (e) { - obj = null; - } - } - - if (obj !== null) { - var version = obj.GetVersions().split(','); - version = version[0].split('='); - version = parseFloat(version[1]); - return version; - } else { - return null; - } - } else { - for (var i = 0; i < navigator.plugins.length; i++) { - if (navigator.plugins[i].name.indexOf('Adobe Acrobat') != -1) { - return navigator.plugins[i].description.replace(/\D+/g, '.').match(/^.?(.+),?$/)[1]; - } - } - - return null; - } - }; - - ft.silverlightVersion = function (m) { - var i; - if (m) { - return null; - } - - var parts = ['', '', '', '']; - var nav = navigator.plugins['Silverlight Plug-In']; - - if (nav) { - for (i = 0; i < 4; i++) { - parts[i] = parseInt(nav.description.split('.')[i]).toString(); - } - } else { - try { - var control = new window.ActiveXObject('AgControl.AgControl'); - var vers = [1, 0, 0, 0]; - loopMatch(control, vers, 0, 1); - loopMatch(control, vers, 1, 1); - loopMatch(control, vers, 2, 10000); - loopMatch(control, vers, 2, 1000); - loopMatch(control, vers, 2, 100); - loopMatch(control, vers, 2, 10); - loopMatch(control, vers, 2, 1); - loopMatch(control, vers, 3, 1); - - for (i = 0; i < 4; i++) { - parts[i] = vers[i].toString(); - } - } catch (e) { - return null; - } - } - - return parts.join('.'); - - function loopMatch(control, vers, idx, inc) { - while (IsSupported(control, vers)) { - vers[idx] += inc; - } - - vers[idx] -= inc; - } - - function IsSupported(control, ver) { - return control.isVersionSupported(ver[0] + '.' + ver[1] + '.' + ver[2] + '.' + ver[3]); - } - }; - - ft.getPlugins = function (m) { - var a = []; - - if (m) { - return a; - } - - try { - for (var i = 0; i < navigator.plugins.length; i++) { - a.push(navigator.plugins[i].name + ': ' + navigator.plugins[i].description + ' (' + navigator.plugins[i].filename + ')'); - } - - return a; - } catch (e) { - return null; - } - }; - - ft.getMimeTypes = function (m) { - var a = []; - - if (m) { - return a; - } - - try { - for (var i = 0; i < navigator.mimeTypes.length; i++) { - a.push(navigator.mimeTypes[i].type + ': ' + navigator.mimeTypes[i].description); - } - - return a; - } catch (e) { - return null; - } - }; - - return ft; - }, - go: (D9r, signals) => { - let tagHost = 'd9.flashtalking.com'; - - function D9request(D9Device) { - var json = encodeURIComponent(JSON.stringify(D9Device)); - var send = '&tbx=' + encodeURIComponent(json); - ajax(getLgcUrl(), send); - } - - function getLgcUrl() { - var httpProto = 'https://'; - var lgcUrl = tagHost + '/lgc'; - return httpProto + lgcUrl; - } - - function getConnectUrl() { - var httpProto = 'https://'; - var lgcUrl = tagHost + '/img/img.png?cnx=' + (device && device.D9_61 ? device.D9_61 : ''); - return httpProto + lgcUrl; - } - - function ajax(url, send) { - if (window.d9PendingXDR != undefined) { - return; - } - - var ar = null; - - function corsSupported() { - try { - return typeof XMLHttpRequest !== 'undefined' && 'withCredentials' in new XMLHttpRequest(); - } catch (e) { - return false; - } - } - - if (typeof window.XDomainRequest !== 'undefined' && !corsSupported()) { - ar = new XDomainRequest(); - ar.open('POST', url); - } else { - try { - ar = new XMLHttpRequest(); - } catch (e) { - if (window.hasOwnProperty('ActiveXObject')) { - var ax = ['Msxml2.XMLHTTP.3.0', 'Msxml2.XMLHTTP.4.0', 'Msxml2.XMLHTTP.6.0', 'Msxml2.XMLHTTP', 'Microsoft.XMLHTTP']; - var i = ax.length; - - while (--i) { - try { - ar = new window.ActiveXObject(ax[i]); - break; - } catch (e) {} - } - } - } - - ar.open('POST', url, true); - ar.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); - ar.withCredentials = true; - } - - ar.onreadystatechange = function () { - if (D9r && D9r.L > 0) { - return; - } - - if (D9r && D9r.callback && ar.readyState == 4) { - if (ar.status == 200 || ar.status == 304) { - var r = ar.responseText; - var response; - - if (r && r.length > 0) { - try { - response = JSON.parse(r); - } catch (e) {} - } - - if (response) { - if (response.cnx) { - new Image().src = getConnectUrl(); - delete response.cnx; - } - - D9r.callback(response); - } else { - D9r.callback(null); - } - } - } - }; - - if (typeof window.XDomainRequest !== 'undefined' && !corsSupported()) { - ar.ontimeout = ar.onerror = function (e) { - ar.status = 400; - ar.readyState = 4; - ar.onreadystatechange(); - }; - - ar.onload = function () { - ar.status = 200; - ar.readyState = 4; - ar.onreadystatechange(); - }; - - ar.onprogress = function () {}; - } - - ar.send(send); - window.d9PendingXDR = ar; - }; - - var device = {}; - device.D9_1 = signals.D9_110; - device.D9_6 = signals.D9_130; - device.D9_7 = signals.D9_131; - device.D9_8 = signals.D9_132; - device.D9_9 = signals.D9_133; - device.D9_10 = signals.D9_134; - device.D9_61 = signals.D9_138; - device.D9_67 = signals.D9_139; - device.D9_18 = {}; - device.D9_16 = signals.D9_111; - - if (signals.D9_101 || signals.D9_111) { - device.D9_4 = { - width: signals.D9_101, - height: signals.D9_102 - }; - } - - if (window.navigator) { - device.D9_14 = signals.D9_120; - device.D9_15 = signals.D9_121; - device.D9_19 = signals.D9_122; - } - - device.D9_33 = signals.D9_150; - device.D9_34 = signals.D9_151; - device.D9_30 = []; - device.D9_52 = {}; - device.D9_57 = typeof D9r.callback === 'function'; - device.D9_58 = D9r; - device.D9_59 = { CampID: 3175, CCampID: 156515 }; - device.D9_63 = signals.D9_140; - device.D9_64 = signals.D9_103; - device.D9_66 = signals.D9_141; - D9request(device); - } - }; - - ajax( - 'https://e.flashtalking.com/cache', - { - success: response => { - cacheId = JSON.parse(response).cache_id || ''; - ftrack.init(cacheId); - }, - error: error => { - ftrack.init(error); - } - } - ); + if (config.params && config.params.url && config.params.url === FTRACK_URL) { + var ftrackScript = document.createElement('script'); + ftrackScript.setAttribute('src', config.params.url); + window.document.body.appendChild(ftrackScript); + } } }; }, @@ -701,73 +107,78 @@ export const ftrackIdSubmodule = { * @returns {IdResponse|undefined} */ extendId (config, consentData, cacheIdObj) { - isConfigOk(config); + this.isConfigOk(config); return cacheIdObj; - } -}; - -/* - * Validates the config, if it is not correct, then info cannot be saved in localstorage - * @function isConfigOk - * @param {SubmoduleConfig} config from HTML - * @returns {true|false} - */ -const isConfigOk = function(config) { - if (!config.storage || !config.storage.type || !config.storage.name) { - utils.logError(LOG_PREFIX + 'storage required to be set'); - return false; - } - - // in a future release, we may return false if storage type or name are not set as required - if (config.storage.type !== LOCAL_STORAGE) { - utils.logWarn(LOG_PREFIX + `storage type recommended to be '${LOCAL_STORAGE}'.`); - } - // in a future release, we may return false if storage type or name are not set as required - if (config.storage.name !== FTRACK_STORAGE_NAME) { - utils.logWarn(LOG_PREFIX + `storage name recommended to be '${FTRACK_STORAGE_NAME}'.`); - } - - return true; -}; - -const isThereConsent = function(consentData) { - let consentValue = true; + }, /* - * Scenario 1: GDPR - * if GDPR Applies is true|1, we do not have consent - * if GDPR Applies does not exist or is false|0, we do not NOT have consent + * Validates the config, if it is not correct, then info cannot be saved in localstorage + * @function isConfigOk + * @param {SubmoduleConfig} config from HTML + * @returns {true|false} */ - if (consentData && consentData.gdprApplies && (consentData.gdprApplies === true || consentData.gdprApplies === 1)) { - consentInfo.gdpr.applies = 1; - consentValue = false; - } - // If consentString exists, then we store it even though we are not using it - if (consentData && consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) { - consentInfo.gdpr.consentString = consentData.consentString; - } + isConfigOk: function(config) { + if (!config.storage || !config.storage.type || !config.storage.name) { + utils.logError(LOG_PREFIX + 'config.storage required to be set.'); + return false; + } + + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.type !== LOCAL_STORAGE) { + utils.logWarn(LOG_PREFIX + 'config.storage.type recommended to be "' + LOCAL_STORAGE + '".'); + } + // in a future release, we may return false if storage type or name are not set as required + if (config.storage.name !== FTRACK_STORAGE_NAME) { + utils.logWarn(LOG_PREFIX + 'config.storage.name recommended to be "' + FTRACK_STORAGE_NAME + '".'); + } + + if (!config.hasOwnProperty('params') || !config.params.hasOwnProperty('url') || config.params.url !== FTRACK_URL) { + utils.logWarn(LOG_PREFIX + 'config.params.url is required for ftrack to run. Url should be "' + FTRACK_URL + '".'); + return false; + } + + return true; + }, - /* - * Scenario 2: CCPA/us_privacy - * if usp exists (assuming this check determines the location of the device to be within the California) - * parse the us_privacy string to see if we have consent - * for version 1 of us_privacy strings, if 'Opt-Out Sale' is 'Y' we do not track - */ - const usp = uspDataHandler.getConsentData(); - let usPrivacyVersion; - // let usPrivacyOptOut; - let usPrivacyOptOutSale; - // let usPrivacyLSPA; - if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) { - consentInfo.usPrivacy.value = usp; - usPrivacyVersion = usp[0]; - // usPrivacyOptOut = usp[1]; - usPrivacyOptOutSale = usp[2]; - // usPrivacyLSPA = usp[3]; + isThereConsent: function(consentData) { + let consentValue = true; + + /* + * Scenario 1: GDPR + * if GDPR Applies is true|1, we do not have consent + * if GDPR Applies does not exist or is false|0, we do not NOT have consent + */ + if (consentData && consentData.gdprApplies && (consentData.gdprApplies === true || consentData.gdprApplies === 1)) { + consentInfo.gdpr.applies = 1; + consentValue = false; + } + // If consentString exists, then we store it even though we are not using it + if (consentData && consentData.consentString !== 'undefined' && !utils.isEmpty(consentData.consentString) && !utils.isEmptyStr(consentData.consentString)) { + consentInfo.gdpr.consentString = consentData.consentString; + } + + /* + * Scenario 2: CCPA/us_privacy + * if usp exists (assuming this check determines the location of the device to be within the California) + * parse the us_privacy string to see if we have consent + * for version 1 of us_privacy strings, if 'Opt-Out Sale' is 'Y' we do not track + */ + const usp = uspDataHandler.getConsentData(); + let usPrivacyVersion; + // let usPrivacyOptOut; + let usPrivacyOptOutSale; + // let usPrivacyLSPA; + if (typeof usp !== 'undefined' && !utils.isEmpty(usp) && !utils.isEmptyStr(usp)) { + consentInfo.usPrivacy.value = usp; + usPrivacyVersion = usp[0]; + // usPrivacyOptOut = usp[1]; + usPrivacyOptOutSale = usp[2]; + // usPrivacyLSPA = usp[3]; + } + if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false; + + return consentValue; } - if (usPrivacyVersion == 1 && usPrivacyOptOutSale === 'Y') consentValue = false; - - return consentValue; }; submodule('userId', ftrackIdSubmodule); diff --git a/modules/ftrackIdSystem.md b/modules/ftrackIdSystem.md index 983262aed3d..90e25dfc7ae 100644 --- a/modules/ftrackIdSystem.md +++ b/modules/ftrackIdSystem.md @@ -42,6 +42,9 @@ pbjs.setConfig({ userSync: { userIds: [{ name: 'FTrack', + params: { + url: 'https://d9.flashtalking.com/d9core' // required, if not populated ftrack will not run + }, storage: { type: 'html5', // "html5" is the required storage type name: 'FTrackId', // "FTrackId" is the required storage name diff --git a/modules/userId/userId.md b/modules/userId/userId.md index c582262b5f2..6ad9bcd94f5 100644 --- a/modules/userId/userId.md +++ b/modules/userId/userId.md @@ -52,6 +52,9 @@ pbjs.setConfig({ expires: 90, refreshInSeconds: 8*3600 }, + params: { + url: 'https://d9.flashtalking.com/d9core', // required, if not populated ftrack will not run + } }, { name: 'parrableId', params: { diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js index b9d3099386a..65cbee387b5 100644 --- a/test/spec/modules/ftrackIdSystem_spec.js +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -7,6 +7,9 @@ let server; let configMock = { name: 'ftrack', + params: { + url: 'https://d9.flashtalking.com/d9core' + }, storage: { name: 'ftrackId', type: 'html5', @@ -28,163 +31,177 @@ describe('FTRACK ID System', () => { }); }); - describe('Publisher config:', () => { + describe('ftrackIdSubmodule.isConfigOk():', () => { let logWarnStub; let logErrorStub; + beforeEach(() => { logWarnStub = sinon.stub(utils, 'logWarn'); logErrorStub = sinon.stub(utils, 'logError'); }); + afterEach(() => { logWarnStub.restore(); logErrorStub.restore(); }); - it(`should be rejected if 'storage' property is missing`, () => { - expect(ftrackIdSubmodule.getId({name: 'ftrack'})).to.be.undefined; - expect(logErrorStub.args[0][0]).to.equal(`FTRACK - storage required to be set`); - }); - - it(`should be rejected if 'storage.type' property is missing`, () => { - expect(ftrackIdSubmodule.getId({ - name: 'ftrack', - storage: { - type: 'html5', - expires: 90, - refreshInSeconds: 8 * 3600 - } - }, null, null)).to.be.undefined; - expect(logErrorStub.args[0][0]).to.equal(`FTRACK - storage required to be set`); - }); - - it(`should be rejected if 'storage.name' property is missing`, () => { - expect(ftrackIdSubmodule.getId({ - name: 'ftrack', - storage: { - name: 'ftrackId', - expires: 90, - refreshInSeconds: 8 * 3600 - } - }, null, null)).to.be.undefined; - expect(logErrorStub.args[0][0]).to.equal(`FTRACK - storage required to be set`); - }); - - it(`should be rejected if 'storage.name' is not 'ftrackId'`, () => { - expect(ftrackIdSubmodule.getId({ - name: 'ftrack', - storage: { - name: 'lorem ipsum', - type: 'html5', - expires: 90, - refreshInSeconds: 8 * 3600 - } - }, null, null).callback()).to.equal(undefined); - expect(logWarnStub.args[0][0]).to.equal(`FTRACK - storage name recommended to be 'ftrackId'.`); - }); - - it(`should be rejected if 'storage.type' is not 'html5'`, () => { - expect(ftrackIdSubmodule.getId({ - name: 'ftrack', - storage: { - name: 'ftrackId', - type: 'cookies', - expires: 90, - refreshInSeconds: 8 * 3600 - } - }, null, null).callback()).to.equal(undefined); - expect(logWarnStub.args[0][0]).to.equal(`FTRACK - storage type recommended to be 'html5'.`); + it(`should be rejected if 'config.storage' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage; + delete configMock1.params; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); }); - }); - describe('getId() method', () => { - it(`should be using the StorageManager to set cookies or localstorage, as opposed to doing it directly`, () => { - expect((/localStorage/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; - expect((/cookie/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + it(`should be rejected if 'config.storage.name' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage.name; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); }); - describe(`endpoint tests - `, () => { - let cacheUrlRegExp = /https:\/\/e\.flashtalking\.com\/cache/; - beforeEach(() => { - server = sinon.createFakeServer(); - }); + it(`should be rejected if 'config.storage.name' is not 'ftrackId'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.storage.name = "not-ftrack"; - afterEach(() => { - server.restore(); - }); + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.name recommended to be "ftrackId".`); + }); - it(`should request the cacheId from the '/cache' endpoint`, () => { - ftrackIdSubmodule.getId(configMock, null, null).callback(); - expect((cacheUrlRegExp).test(server.requests[0].url)).to.be.ok; - }); + it(`should be rejected if 'congig.storage.type' property is missing`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.storage.type; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logErrorStub.args[0][0]).to.equal(`FTRACK - config.storage required to be set.`); + }); + + it(`should be rejected if 'config.storage.type' is not 'html5'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.storage.type = "not-html5"; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.type recommended to be "html5".`); + }); + + it(`should be rejected if 'config.params.url' does not exist`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + delete configMock1.params.url; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`); + }); + + it(`should be rejected if 'storage.param.url' does not exist or is not 'https://d9.flashtalking.com/d9core'`, () => { + let configMock1 = JSON.parse(JSON.stringify(configMock)); + configMock1.params.url = 'https://d9.NOT.flashtalking.com/d9core'; + + ftrackIdSubmodule.isConfigOk(configMock1); + expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.params.url is required for ftrack to run. Url should be "https://d9.flashtalking.com/d9core".`); + }); + }); - it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => { - ftrackIdSubmodule.getId(configMock, null, null).callback(); - expect(server.requests).to.have.length(1); - server.resetHistory(); + describe(`ftrackIdSubmodule.isThereConsent():`, () => { + let uspDataHandlerStub; + beforeEach(() => { + uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + }); - ftrackIdSubmodule.decode('value', configMock); - expect(server.requests).to.have.length(0); - server.resetHistory(); + afterEach(() => { + uspDataHandlerStub.restore(); + }); - ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); - expect(server.requests).to.have.length(0); + describe(`returns 'false' if:`, () => { + it(`GDPR: if gdprApplies is truthy`, () => { + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 1})).to.not.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: true})).to.not.be.ok; }); - it(`should populate localstorage (end-to-end test)`, () => { - let lgcResponseMock = { - 'DeviceID': [''], - 'SingleDeviceID': [''] - }; - ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); - expect((cacheUrlRegExp).test(server.requests[0].url)).to.be.ok; - server.requests[0].respond(200, { 'Content-Type': 'application/json' }, '{"cache_id":""}'); - expect((/lgc/).test(server.requests[1].url)).to.be.ok; - server.requests[1].respond(200, { 'Content-Type': 'application/json' }, JSON.stringify(lgcResponseMock)); - - expect(localStorage.getItem('ftrackId')).to.equal(JSON.stringify(lgcResponseMock)); - expect(localStorage.getItem('ftrackId_exp')).to.be.ok; - expect(localStorage.getItem('ftrackId_privacy')).to.equal(JSON.stringify({'gdpr': {'applies': 0, 'consentString': '', 'pd': null}, 'usPrivacy': {'value': null}})); - expect(localStorage.getItem('ftrackId_privacy_exp')).to.be.ok; + it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => { + uspDataHandlerStub.returns('1YYY'); + expect(ftrackIdSubmodule.isThereConsent({})).to.not.be.ok; }); }); - describe(`consent options - `, () => { - let uspDataHandlerStub; - beforeEach(() => { - uspDataHandlerStub = sinon.stub(uspDataHandler, 'getConsentData'); + describe(`returns 'true' if`, () => { + it(`GDPR: if gdprApplies is undefined, false or 0`, () => { + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: 0})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: false})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({gdprApplies: null})).to.be.ok; + expect(ftrackIdSubmodule.isThereConsent({})).to.be.ok; }); - afterEach(() => { - uspDataHandlerStub.restore(); + it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => { + uspDataHandlerStub.returns('1NNN'); + expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; + + uspDataHandlerStub.returns('1---'); + expect(ftrackIdSubmodule.isThereConsent(null)).to.be.ok; }); + }); + }); + + describe('getId() method', () => { + it(`should be using the StorageManager to set cookies or localstorage, as opposed to doing it directly`, () => { + expect((/localStorage/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + expect((/cookie/gi).test(JSON.stringify(ftrackIdSubmodule))).to.not.be.ok; + }); - describe(`getId() should return undefined`, () => { - it(`GDPR: if gdprApplies is truthy`, () => { - expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: 1}, null)).to.not.be.ok; - expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: true}, null)).to.not.be.ok; - }); + it(`should be the only method that gets a new ID aka hits the D9 endpoint`, () => { + let appendChildStub = sinon.stub(window.document.body, 'appendChild'); - it(`US_PRIVACY version 1: if 'Opt Out Sale' is 'Y'`, () => { - uspDataHandlerStub.returns('1YYY'); - expect(ftrackIdSubmodule.getId(configMock, {}, null)).to.not.be.ok; - }); - }); + ftrackIdSubmodule.getId(configMock, null, null).callback(); + expect(window.document.body.appendChild.called).to.be.ok; + let actualScriptTag = window.document.body.appendChild.args[0][0]; + expect(actualScriptTag.tagName.toLowerCase()).to.equal("script"); + expect(actualScriptTag.getAttribute("src")).to.equal("https://d9.flashtalking.com/d9core"); + appendChildStub.resetHistory(); + + ftrackIdSubmodule.decode('value', configMock); + expect(window.document.body.appendChild.called).to.not.be.ok; + expect(window.document.body.appendChild.args).to.deep.equal([]); + appendChildStub.resetHistory(); + + ftrackIdSubmodule.extendId(configMock, null, {cache: {id: ''}}); + expect(window.document.body.appendChild.called).to.not.be.ok; + expect(window.document.body.appendChild.args).to.deep.equal([]); - describe(`getId() should run`, () => { - it(`GDPR: if gdprApplies is undefined, false or 0`, () => { - expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: 0}, null)).to.be.ok; - expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: false}, null)).to.be.ok; - expect(ftrackIdSubmodule.getId(configMock, {gdprApplies: null}, null)).to.be.ok; - expect(ftrackIdSubmodule.getId(configMock, {}, null)).to.be.ok; - }); - - it(`US_PRIVACY version 1: if 'Opt Out Sale' is not 'Y' ('N','-')`, () => { - uspDataHandlerStub.returns('1NNN'); - expect(ftrackIdSubmodule.getId(configMock, null, null)).to.be.ok; - - uspDataHandlerStub.returns('1---'); - expect(ftrackIdSubmodule.getId(configMock, null, null)).to.be.ok; - }); + appendChildStub.restore(); + }); + + it(`should populate localstorage and return the IDS (end-to-end test)`, () => { + let ftrackId, + ftrackIdExp, + forceCallback = false; + + // Confirm that our item is not in localStorage yet + expect(window.localStorage.getItem('ftrack-rtd')).to.not.be.ok; + expect(window.localStorage.getItem('ftrack-rtd_exp')).to.not.be.ok; + + ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); + return new Promise(function(resolve, reject) { + window.testTimer = function () { + // Sinon fake server is changing the readyState to 4, so instead + // we are forcing the callback + if (!forceCallback && window.hasOwnProperty("D9r")) { + window.D9r.callback({ "DeviceID": [""], "SingleDeviceID": [""] }); + forceCallback = true; + } + + ftrackId = window.localStorage.getItem('ftrackId'); + ftrackIdExp = window.localStorage.getItem('ftrackId_exp'); + + if (!!ftrackId && !!ftrackIdExp) { + expect(window.localStorage.getItem('ftrackId')).to.be.ok; + expect(window.localStorage.getItem('ftrackId_exp')).to.be.ok; + resolve(); + } else { + window.setTimeout(window.testTimer, 25); + } + }; + window.testTimer(); }); }); }); From 3dfecd8e2031e2b43c199c1c1b537f1821f6df1a Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Thu, 24 Feb 2022 16:23:35 -0500 Subject: [PATCH 6/9] Cleaning up errors raised by linter --- modules/ftrackIdSystem.js | 2 +- test/spec/modules/ftrackIdSystem_spec.js | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/modules/ftrackIdSystem.js b/modules/ftrackIdSystem.js index a057f0d2474..3f2182a8b98 100644 --- a/modules/ftrackIdSystem.js +++ b/modules/ftrackIdSystem.js @@ -136,7 +136,7 @@ export const ftrackIdSubmodule = { utils.logWarn(LOG_PREFIX + 'config.params.url is required for ftrack to run. Url should be "' + FTRACK_URL + '".'); return false; } - + return true; }, diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js index 65cbee387b5..f7a868218f7 100644 --- a/test/spec/modules/ftrackIdSystem_spec.js +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -64,7 +64,7 @@ describe('FTRACK ID System', () => { it(`should be rejected if 'config.storage.name' is not 'ftrackId'`, () => { let configMock1 = JSON.parse(JSON.stringify(configMock)); - configMock1.storage.name = "not-ftrack"; + configMock1.storage.name = 'not-ftrack'; ftrackIdSubmodule.isConfigOk(configMock1); expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.name recommended to be "ftrackId".`); @@ -80,7 +80,7 @@ describe('FTRACK ID System', () => { it(`should be rejected if 'config.storage.type' is not 'html5'`, () => { let configMock1 = JSON.parse(JSON.stringify(configMock)); - configMock1.storage.type = "not-html5"; + configMock1.storage.type = 'not-html5'; ftrackIdSubmodule.isConfigOk(configMock1); expect(logWarnStub.args[0][0]).to.equal(`FTRACK - config.storage.type recommended to be "html5".`); @@ -155,8 +155,8 @@ describe('FTRACK ID System', () => { ftrackIdSubmodule.getId(configMock, null, null).callback(); expect(window.document.body.appendChild.called).to.be.ok; let actualScriptTag = window.document.body.appendChild.args[0][0]; - expect(actualScriptTag.tagName.toLowerCase()).to.equal("script"); - expect(actualScriptTag.getAttribute("src")).to.equal("https://d9.flashtalking.com/d9core"); + expect(actualScriptTag.tagName.toLowerCase()).to.equal('script'); + expect(actualScriptTag.getAttribute('src')).to.equal('https://d9.flashtalking.com/d9core'); appendChildStub.resetHistory(); ftrackIdSubmodule.decode('value', configMock); @@ -185,8 +185,8 @@ describe('FTRACK ID System', () => { window.testTimer = function () { // Sinon fake server is changing the readyState to 4, so instead // we are forcing the callback - if (!forceCallback && window.hasOwnProperty("D9r")) { - window.D9r.callback({ "DeviceID": [""], "SingleDeviceID": [""] }); + if (!forceCallback && window.hasOwnProperty('D9r')) { + window.D9r.callback({ 'DeviceID': [''], 'SingleDeviceID': [''] }); forceCallback = true; } From f86307b01f55e9b26cfdf912d7174caa5140c325 Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Thu, 24 Feb 2022 16:47:29 -0500 Subject: [PATCH 7/9] Tweaking a comment --- test/spec/modules/ftrackIdSystem_spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/spec/modules/ftrackIdSystem_spec.js b/test/spec/modules/ftrackIdSystem_spec.js index f7a868218f7..69d66d75bb1 100644 --- a/test/spec/modules/ftrackIdSystem_spec.js +++ b/test/spec/modules/ftrackIdSystem_spec.js @@ -183,8 +183,8 @@ describe('FTRACK ID System', () => { ftrackIdSubmodule.getId(configMock, consentDataMock, null).callback(); return new Promise(function(resolve, reject) { window.testTimer = function () { - // Sinon fake server is changing the readyState to 4, so instead - // we are forcing the callback + // Sinon fake server is NOT changing the readyState to 4, so instead + // we are forcing the callback to run and just passing in the expected Object if (!forceCallback && window.hasOwnProperty('D9r')) { window.D9r.callback({ 'DeviceID': [''], 'SingleDeviceID': [''] }); forceCallback = true; From 9ef560878a99d457785a395ef931efe2560db437 Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Thu, 10 Mar 2022 09:18:34 -0500 Subject: [PATCH 8/9] JDB-496: cleaning up docs based on PR feedback --- modules/ftrackIdSystem.md | 18 ------------------ modules/userId/eids.js | 2 +- modules/userId/eids.md | 2 +- 3 files changed, 2 insertions(+), 20 deletions(-) diff --git a/modules/ftrackIdSystem.md b/modules/ftrackIdSystem.md index 90e25dfc7ae..c5f255c2fc2 100644 --- a/modules/ftrackIdSystem.md +++ b/modules/ftrackIdSystem.md @@ -2,29 +2,16 @@ *The FTrack Identity Framework User ID Module allows publishers to take advantage of Flashtalking's FTrack ID during the bidding process.* -**THE ONLY COMPLETE SOLUTION FOR COOKIELESS MEASUREMENT & PERSONALIZATION** - -Flashtalking is the world’s first ad serving platform to function without cookies to orchestrate client identity across buy-side ID spaces for measurement and personalization. With over 120 active global advertisers, our cookieless identity framework is market-ready and includes privacy controls to ensure consumer notification and choice on every impression. - ### [FTrack](https://www.flashtalking.com/identity-framework#FTrack) Flashtalking’s cookieless tracking technology uses probabilistic device recognition to derive a privacy-friendly persistent ID for each device. -**PROVEN** -With over 120 brands using FTrack globally, Flashtalking has accumulated the largest cookieless footprint in the industry. - **ANTI-FINGERPRINTING** FTrack operates in strict compliance with [Google’s definition of anti-fingerprinting](https://blog.google/products/ads-commerce/2021-01-privacy-sandbox/). FTrack does not access PII or sensitive information and provides consumers with notification and choice on every impression. We do not participate in the types of activities that most concern privacy advocates (profiling consumers, building audience segments, and/or monetizing consumer data). **GDPR COMPLIANT** Flashtalking is integrated with the IAB EU’s Transparency & Consent Framework (TCF) and operates on a Consent legal basis where required. As a Data Processor under GDPR, Flashtalking does not combine data across customers nor sell data to third parties. -**ACCURATE** -FTrack’s broad adoption combined with the maturity of the models (6+ years old) gives Flashtalking the global scale with which to maintain a high degree of model resolution and accuracy. - -**DURABLE** -As new IDs start to proliferate, they will serve as new incremental signals for our models. - --- ### Support or Maintenance: @@ -57,9 +44,6 @@ pbjs.setConfig({ }); ``` - - - | Param under userSync.userIds[] | Scope | Type | Description | Example | | :-- | :-- | :-- | :-- | :-- | | name | Required | String | The name of this module: `"FTrack"` | `"FTrack"` | @@ -69,8 +53,6 @@ pbjs.setConfig({ | storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. FTrack recommends `90`. | `90` | | storage.refreshInSeconds | Optional | Integer | How many seconds until the FTrack ID will be refreshed. FTrack strongly recommends 8 hours between refreshes | `8*3600` | -**ATTENTION:** As of Prebid.js v4.14.0, FTrack requires `storage.type` to be `"html5"` and `storage.name` to be `"FTrackId"`. Using other values will display a warning today, but in an upcoming release, it will prevent the FTrack module from loading. This change is to ensure the FTrack module in Prebid.js interoperates properly with the [FTrack](https://www.flashtalking.com/identity-framework#FTrack) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [prebid-support@flashtalking.com](mailto:prebid-support@flashtalking.com). - --- ### Privacy Policies. diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 05d42537a3b..9064d697a2c 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -50,7 +50,7 @@ const USER_IDS_CONFIG = { // ftrack 'ftrackId': { - source: 'flashtalking.com', + source: 'flashtalking.com/identity-framework#FTrack', atype: 1, getValue: function(data) { return data.uid diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 29f7d8c7064..40c04e961ee 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -50,7 +50,7 @@ userIdAsEids = [ }, { - source: 'ftrack.com', + source: 'flashtalking.com/identity-framework#FTrack', uids: [{ id: 'some-random-id-value', atype: 1 From 21e5a9aedb71120add8c94276ea673d501f5e04a Mon Sep 17 00:00:00 2001 From: Jason Lydon Date: Thu, 10 Mar 2022 10:57:22 -0500 Subject: [PATCH 9/9] PR fixes --- modules/userId/eids.js | 2 +- modules/userId/eids.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 9064d697a2c..05d42537a3b 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -50,7 +50,7 @@ const USER_IDS_CONFIG = { // ftrack 'ftrackId': { - source: 'flashtalking.com/identity-framework#FTrack', + source: 'flashtalking.com', atype: 1, getValue: function(data) { return data.uid diff --git a/modules/userId/eids.md b/modules/userId/eids.md index 40c04e961ee..11720a7afff 100644 --- a/modules/userId/eids.md +++ b/modules/userId/eids.md @@ -50,7 +50,7 @@ userIdAsEids = [ }, { - source: 'flashtalking.com/identity-framework#FTrack', + source: 'flashtalking.com', uids: [{ id: 'some-random-id-value', atype: 1