From 6fc52d6297b03177f6c3daef2100fe7d0fbbf691 Mon Sep 17 00:00:00 2001 From: talbotja Date: Thu, 24 Oct 2024 11:58:16 +0100 Subject: [PATCH] Permutive Identity Manager: initial implementation (#12337) * Implement permutiveIdSystem userId submodule * minor changes following internal review * rename permutiveId -> permutiveIdentityManagerId emphasizes that permutive is not actually providing any IDs itself --- modules/.submodules.json | 1 + modules/permutiveIdentityManagerIdSystem.js | 151 ++++++++++++++++++ modules/permutiveIdentityManagerIdSystem.md | 58 +++++++ .../permutiveIdentityManagerIdSystem_spec.js | 126 +++++++++++++++ 4 files changed, 336 insertions(+) create mode 100644 modules/permutiveIdentityManagerIdSystem.js create mode 100644 modules/permutiveIdentityManagerIdSystem.md create mode 100644 test/spec/modules/permutiveIdentityManagerIdSystem_spec.js diff --git a/modules/.submodules.json b/modules/.submodules.json index 36daa70e75b..d998a62500a 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -36,6 +36,7 @@ "novatiqIdSystem", "oneKeyIdSystem", "operaadsIdSystem", + "permutiveIdentityManagerIdSystem", "pubProvidedIdSystem", "publinkIdSystem", "quantcastIdSystem", diff --git a/modules/permutiveIdentityManagerIdSystem.js b/modules/permutiveIdentityManagerIdSystem.js new file mode 100644 index 00000000000..5dc12d44edb --- /dev/null +++ b/modules/permutiveIdentityManagerIdSystem.js @@ -0,0 +1,151 @@ +import {MODULE_TYPE_UID} from '../src/activities/modules.js' +import {submodule} from '../src/hook.js' +import {getStorageManager} from '../src/storageManager.js' +import {prefixLog, safeJSONParse} from '../src/utils.js' +/** + * @typedef {import('../modules/userId/index.js').Submodule} Submodule + * @typedef {import('../modules/userId/index.js').SubmoduleConfig} SubmoduleConfig + * @typedef {import('../modules/userId/index.js').ConsentData} ConsentData + * @typedef {import('../modules/userId/index.js').IdResponse} IdResponse + */ + +const MODULE_NAME = 'permutiveIdentityManagerId' +const PERMUTIVE_ID_DATA_STORAGE_KEY = 'permutive-prebid-id' + +const ID5_DOMAIN = 'id5-sync.com' +const LIVERAMP_DOMAIN = 'liveramp.com' +const UID_DOMAIN = 'uidapi.com' + +const PRIMARY_IDS = ['id5id', 'idl_env', 'uid2'] + +export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME}) + +const logger = prefixLog('[PermutiveID]') + +const readFromSdkLocalStorage = () => { + const data = safeJSONParse(storage.getDataFromLocalStorage(PERMUTIVE_ID_DATA_STORAGE_KEY)) + const id = {} + if (data && typeof data === 'object' && 'providers' in data && typeof data.providers === 'object') { + const now = Date.now() + for (const [idName, value] of Object.entries(data.providers)) { + if (PRIMARY_IDS.includes(idName) && value.userId) { + if (!value.expiryTime || value.expiryTime > now) { + id[idName] = value.userId + } + } + } + } + return id +} + +/** + * Catch and log errors + * @param {function} fn - Function to safely evaluate + */ +function makeSafe (fn) { + try { + return fn() + } catch (e) { + logger.logError(e) + } +} + +const waitAndRetrieveFromSdk = (timeoutMs) => + new Promise( + resolve => { + const fallback = setTimeout(() => { + logger.logInfo('timeout expired waiting for SDK - attempting read from local storage again') + resolve(readFromSdkLocalStorage()) + }, timeoutMs) + return window?.permutive?.ready(() => makeSafe(() => { + logger.logInfo('Permutive SDK is ready') + const onReady = makeSafe(() => window.permutive.addons.identity_manager.prebid.onReady) + if (typeof onReady === 'function') { + onReady((ids) => { + logger.logInfo('Permutive SDK has provided ids') + resolve(ids) + clearTimeout(fallback) + }) + } else { + logger.logError('Permutive SDK initialised but identity manager prebid api not present') + } + })) + } + ) + +/** @type {Submodule} */ +export const permutiveIdentityManagerIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + + /** + * decode the stored id value for passing to bid requests + * @function decode + * @param {(Object|string)} value + * @param {SubmoduleConfig|undefined} config + * @returns {(Object|undefined)} + */ + decode(value, config) { + return value + }, + + /** + * performs action to obtain id and return a value in the callback's response argument + * @function getId + * @param {SubmoduleConfig} submoduleConfig + * @param {ConsentData} consentData + * @param {(Object|undefined)} cacheIdObj + * @returns {IdResponse|undefined} + */ + getId(submoduleConfig, consentData, cacheIdObj) { + const id = readFromSdkLocalStorage() + if (Object.entries(id).length > 0) { + logger.logInfo('found id in sdk storage') + return { id } + } else if ('params' in submoduleConfig && submoduleConfig.params.ajaxTimeout) { + logger.logInfo('failed to find id in sdk storage - waiting for sdk') + // Is ajaxTimeout an appropriate timeout to use here? + return { callback: (done) => waitAndRetrieveFromSdk(submoduleConfig.params.ajaxTimeout).then(done) } + } else { + logger.logInfo('failed to find id in sdk storage and no wait time specified') + } + }, + + primaryIds: PRIMARY_IDS, + + eids: { + 'id5id': { + getValue: function (data) { + return data.uid + }, + source: ID5_DOMAIN, + atype: 1, + getUidExt: function (data) { + if (data.ext) { + return data.ext + } + } + }, + 'idl_env': { + source: LIVERAMP_DOMAIN, + atype: 3, + }, + 'uid2': { + source: UID_DOMAIN, + atype: 3, + getValue: function(data) { + return data.id + }, + getUidExt: function(data) { + if (data.ext) { + return data.ext + } + } + } + } +} + +submodule('userId', permutiveIdentityManagerIdSubmodule) diff --git a/modules/permutiveIdentityManagerIdSystem.md b/modules/permutiveIdentityManagerIdSystem.md new file mode 100644 index 00000000000..ae249803d11 --- /dev/null +++ b/modules/permutiveIdentityManagerIdSystem.md @@ -0,0 +1,58 @@ +# Permutive Identity Manager + +This module supports [Permutive](https://permutive.com/) customers in using Permutive's Identity Manager functionality. + +To use this Prebid.js module it is assumed that the site includes Permutive's SDK, with Identity Manager configuration +enabled. See Permutive's user documentation for more information on Identity Manager. + +## Building Prebid.js with Permutive Identity Manager Support + +Prebid.js must be built with the `permutiveIdentityManagerIdSystem` module in order for Permutive's Identity Manager to be able to +activate relevant user identities to Prebid. + +To build Prebid.js with the `permutiveIdentityManagerIdSystem` module included: + +``` +gulp build --modules=userId,permutiveIdentityManagerIdSystem +``` + +## Prebid configuration + +There is minimal configuration required to be set on Prebid.js, since the bulk of the behaviour is managed through +Permutive's dashboard and SDK. + +It is recommended to keep the Prebid.js caching for this module short, since the mechanism by which Permutive's SDK +communicates with Prebid.js is effectively a local cache anyway. + +``` +pbjs.setConfig({ + ... + userSync: { + userIds: [ + { + name: 'permutiveIdentityManagerId', + params: { + ajaxTimeout: 90 + }, + storage: { + type: 'html5', + name: 'permutiveIdentityManagerId', + refreshInSeconds: 5 + } + } + ], + auctionDelay: 100 + }, + ... +}); +``` + +### ajaxTimeout + +By default this module will read IDs provided by the Permutive SDK from local storage when requested by prebid, and if +nothing is found, will not provide any identities. If a timeout is provided via the `ajaxTimeout` parameter, it will +instead wait for up to the specified number of milliseconds for Permutive's SDK to become available, and will retrieve +identities from the SDK directly if/when this happens. + +This value should be set to a value smaller than the `auctionDelay` set on the `userSync` configuration object, since +there is no point waiting longer than this as the auction will already have been triggered. \ No newline at end of file diff --git a/test/spec/modules/permutiveIdentityManagerIdSystem_spec.js b/test/spec/modules/permutiveIdentityManagerIdSystem_spec.js new file mode 100644 index 00000000000..96c581844c1 --- /dev/null +++ b/test/spec/modules/permutiveIdentityManagerIdSystem_spec.js @@ -0,0 +1,126 @@ +import { permutiveIdentityManagerIdSubmodule, storage } from 'modules/permutiveIdentityManagerIdSystem' +import { deepSetValue } from 'src/utils.js' + +const STORAGE_KEY = 'permutive-prebid-id' + +describe('permutiveIdentityManagerIdSystem', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(STORAGE_KEY) + }) + + describe('decode', () => { + it('returns the input unchanged', () => { + const input = { + id5id: { + uid: '0', + ext: { + abTestingControlGroup: false, + linkType: 2, + pba: 'somepba' + } + } + } + const result = permutiveIdentityManagerIdSubmodule.decode(input) + expect(result).to.be.equal(input) + }) + }) + + describe('getId', () => { + it('returns relevant IDs from localStorage and does not return unexpected IDs', () => { + const data = getUserIdData() + storage.setDataInLocalStorage(STORAGE_KEY, JSON.stringify(data)) + const result = permutiveIdentityManagerIdSubmodule.getId({}) + const expected = { + 'id': { + 'id5id': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + } + } + } + expect(result).to.deep.equal(expected) + }) + + it('returns undefined if no relevant IDs are found in localStorage', () => { + storage.setDataInLocalStorage(STORAGE_KEY, '{}') + const result = permutiveIdentityManagerIdSubmodule.getId({}) + expect(result).to.be.undefined + }) + + it('will optionally wait for Permutive SDK if no identities are in local storage already', async () => { + const cleanup = setWindowPermutive() + const result = permutiveIdentityManagerIdSubmodule.getId({params: {ajaxTimeout: 50}}) + expect(result).not.to.be.undefined + expect(result.id).to.be.undefined + expect(result.callback).not.to.be.undefined + const expected = { + 'id5id': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + } + } + const r = await new Promise(result.callback) + expect(r).to.deep.equal(expected) + cleanup() + }) + }) +}) + +const setWindowPermutive = () => { + // Read from Permutive + const backup = window.permutive + + deepSetValue(window, 'permutive.ready', (f) => { + setTimeout(() => f(), 5) + }) + + deepSetValue(window, 'permutive.addons.identity_manager.prebid.onReady', (f) => { + setTimeout(() => f(sdkUserIdData()), 5) + }) + + // Cleanup + return () => window.permutive = backup +} + +const sdkUserIdData = () => ({ + 'id5id': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + }, +}) + +const getUserIdData = () => ({ + 'providers': { + 'id5id': { + 'userId': { + 'uid': '0', + 'linkType': 0, + 'ext': { + 'abTestingControlGroup': false, + 'linkType': 0, + 'pba': 'EVqgf9vY0fSrsrqJZMOm+Q==' + } + } + }, + 'fooid': { + 'userId': { + 'id': '1' + } + } + } +})