diff --git a/modules/.submodules.json b/modules/.submodules.json index cfa98b5ab32..3913f3f5734 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -39,6 +39,7 @@ "teadsIdSystem", "tncIdSystem", "utiqSystem", + "utiqMtpIdSystem", "uid2IdSystem", "euidIdSystem", "unifiedIdSystem", diff --git a/modules/utiqMtpIdSystem.js b/modules/utiqMtpIdSystem.js new file mode 100644 index 00000000000..c5d25f27ca5 --- /dev/null +++ b/modules/utiqMtpIdSystem.js @@ -0,0 +1,138 @@ +/** + * This module adds Utiq MTP provided by Utiq SA/NV to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/utiqMtpIdSystem + * @requires module:modules/userId + */ +import { logInfo } from '../src/utils.js'; +import { submodule } from '../src/hook.js'; +import { getStorageManager } from '../src/storageManager.js'; +import { MODULE_TYPE_UID } from '../src/activities/modules.js'; + +const MODULE_NAME = 'utiqMtpId'; +const LOG_PREFIX = 'Utiq MTP module'; + +export const storage = getStorageManager({ + moduleType: MODULE_TYPE_UID, + moduleName: MODULE_NAME, +}); + +/** + * Get the "mtid" from html5 local storage to make it available to the UserId module. + * @param config + * @returns {{utiqMtp: (*|string)}} + */ +function getUtiqFromStorage() { + let utiqPass; + let utiqPassStorage = JSON.parse( + storage.getDataFromLocalStorage('utiqPass') + ); + logInfo( + `${LOG_PREFIX}: Local storage utiqPass: ${JSON.stringify( + utiqPassStorage + )}` + ); + + if ( + utiqPassStorage && + utiqPassStorage.connectId && + Array.isArray(utiqPassStorage.connectId.idGraph) && + utiqPassStorage.connectId.idGraph.length > 0 + ) { + utiqPass = utiqPassStorage.connectId.idGraph[0]; + } + logInfo( + `${LOG_PREFIX}: Graph of utiqPass: ${JSON.stringify( + utiqPass + )}` + ); + + return { + utiqMtp: + utiqPass && utiqPass.mtid + ? utiqPass.mtid + : null, + }; +} + +/** @type {Submodule} */ +export const utiqMtpIdSubmodule = { + /** + * Used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * Decodes the stored id value for passing to bid requests. + * @function + * @returns {{utiqMtp: string} | null} + */ + decode(bidId) { + logInfo(`${LOG_PREFIX}: Decoded ID value ${JSON.stringify(bidId)}`); + return bidId.utiqMtp ? bidId : null; + }, + /** + * Get the id from helper function and initiate a new user sync. + * @param config + * @returns {{callback: result}|{id: {utiqMtp: string}}} + */ + getId: function (config) { + const data = getUtiqFromStorage(); + if (data.utiqMtp) { + logInfo(`${LOG_PREFIX}: Local storage ID value ${JSON.stringify(data)}`); + return { id: { utiqMtp: data.utiqMtp } }; + } else { + if (!config) { + config = {}; + } + if (!config.params) { + config.params = {}; + } + if ( + typeof config.params.maxDelayTime === 'undefined' || + config.params.maxDelayTime === null + ) { + config.params.maxDelayTime = 1000; + } + // Current delay and delay step in milliseconds + let currentDelay = 0; + const delayStep = 50; + const result = (callback) => { + const data = getUtiqFromStorage(); + if (!data.utiqMtp) { + if (currentDelay > config.params.maxDelayTime) { + logInfo( + `${LOG_PREFIX}: No utiq value set after ${config.params.maxDelayTime} max allowed delay time` + ); + callback(null); + } else { + currentDelay += delayStep; + setTimeout(() => { + result(callback); + }, delayStep); + } + } else { + const dataToReturn = { utiqMtp: data.utiqMtp }; + logInfo( + `${LOG_PREFIX}: Returning ID value data of ${JSON.stringify( + dataToReturn + )}` + ); + callback(dataToReturn); + } + }; + return { callback: result }; + } + }, + eids: { + 'utiqMtp': { + source: 'utiq-mtp.com', + atype: 1, + getValue: function (data) { + return data; + }, + }, + } +}; + +submodule('userId', utiqMtpIdSubmodule); diff --git a/modules/utiqMtpIdSystem.md b/modules/utiqMtpIdSystem.md new file mode 100644 index 00000000000..9b738152969 --- /dev/null +++ b/modules/utiqMtpIdSystem.md @@ -0,0 +1,22 @@ +## Utiq User ID Submodule + +Utiq MTP ID Module. + +### Utiq installation ### + +In order to use utiq in your prebid setup, you must first integrate utiq solution on your website as per https://docs.utiq.com/ +If you are interested in using Utiq on your website, please contact Utiq on https://utiq.com/contact/ + +### Prebid integration ### + +First, make sure to add the utiq MTP submodule to your Prebid.js package with: + +``` +gulp build --modules=userId,adfBidAdapter,ixBidAdapter,prebidServerBidAdapter,utiqMtpIdSystem +``` + +## Parameter Descriptions + +| Params under userSync.userIds[] | Type | Description | Example | +| ------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------ | -------------------------------- | +| name | String | The name of the module | `"utiqMtpId"` | diff --git a/test/spec/modules/utiqMtpIdSystem_spec.js b/test/spec/modules/utiqMtpIdSystem_spec.js new file mode 100644 index 00000000000..0456d485875 --- /dev/null +++ b/test/spec/modules/utiqMtpIdSystem_spec.js @@ -0,0 +1,188 @@ +import { expect } from 'chai'; +import { utiqMtpIdSubmodule } from 'modules/utiqMtpIdSystem.js'; +import { storage } from 'modules/utiqMtpIdSystem.js'; + +describe('utiqMtpIdSystem', () => { + const utiqPassKey = 'utiqPass'; + + const getStorageData = (idGraph) => { + if (!idGraph) { + idGraph = {id: 501, domain: ''}; + } + return { + 'connectId': { + 'idGraph': [idGraph], + } + } + }; + + it('should have the correct module name declared', () => { + expect(utiqMtpIdSubmodule.name).to.equal('utiqMtpId'); + }); + + describe('utiqMtpId getId()', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(utiqPassKey); + }); + + it('it should return object with key callback', () => { + expect(utiqMtpIdSubmodule.getId()).to.have.property('callback'); + }); + + it('should return object with key callback with value type - function', () => { + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData())); + expect(utiqMtpIdSubmodule.getId()).to.have.property('callback'); + expect(typeof utiqMtpIdSubmodule.getId().callback).to.be.equal('function'); + }); + + it('tests if localstorage & JSON works properly ', () => { + const idGraph = { + 'domain': 'domainValue', + 'mtid': 'mtidValue', + }; + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData(idGraph))); + expect(JSON.parse(storage.getDataFromLocalStorage(utiqPassKey))).to.have.property('connectId'); + }); + + it('returns {id: {utiq: data.utiq}} if we have the right data stored in the localstorage ', () => { + const idGraph = { + 'domain': 'test.domain', + 'mtid': 'mtidValue', + }; + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData(idGraph))); + const response = utiqMtpIdSubmodule.getId(); + expect(response).to.have.property('id'); + expect(response.id).to.have.property('utiqMtp'); + expect(response.id.utiqMtp).to.be.equal('mtidValue'); + }); + + it('returns {utiqMtp: data.utiqMtp} if we have the right data stored in the localstorage right after the callback is called', (done) => { + const idGraph = { + 'domain': 'test.domain', + 'mtid': 'mtidValue', + }; + const response = utiqMtpIdSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData(idGraph))); + response.callback(function (result) { + expect(result).to.not.be.null; + expect(result).to.have.property('utiqMtp'); + expect(result.utiqMtp).to.be.equal('mtidValue'); + done() + }) + } + }); + + it('returns {utiqMtp: data.utiqMtp} if we have the right data stored in the localstorage right after 500ms delay', (done) => { + const idGraph = { + 'domain': 'test.domain', + 'mtid': 'mtidValue', + }; + + const response = utiqMtpIdSubmodule.getId(); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData(idGraph))); + }, 500); + response.callback(function (result) { + expect(result).to.not.be.null; + expect(result).to.have.property('utiqMtp'); + expect(result.utiqMtp).to.be.equal('mtidValue'); + done() + }) + } + }); + + it('returns null if we have the data stored in the localstorage after 500ms delay and the max (waiting) delay is only 200ms ', (done) => { + const idGraph = { + 'domain': 'test.domain', + 'mtid': 'mtidValue', + }; + + const response = utiqMtpIdSubmodule.getId({params: {maxDelayTime: 200}}); + expect(response).to.have.property('callback'); + expect(response.callback.toString()).contain('result(callback)'); + + if (typeof response.callback === 'function') { + setTimeout(() => { + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData(idGraph))); + }, 500); + response.callback(function (result) { + expect(result).to.be.null; + done() + }) + } + }); + }); + + describe('utiq decode()', () => { + const VALID_API_RESPONSES = [ + { + expected: '32a97f612', + payload: { + utiqMtp: '32a97f612' + } + }, + { + expected: '32a97f61', + payload: { + utiqMtp: '32a97f61', + } + }, + ]; + VALID_API_RESPONSES.forEach(responseData => { + it('should return a newly constructed object with the utiqMtp for a payload with {utiqMtp: value}', () => { + expect(utiqMtpIdSubmodule.decode(responseData.payload)).to.deep.equal( + {utiqMtp: responseData.expected} + ); + }); + }); + + [{}, '', {foo: 'bar'}].forEach((response) => { + it(`should return null for an invalid response "${JSON.stringify(response)}"`, () => { + expect(utiqMtpIdSubmodule.decode(response)).to.be.null; + }); + }); + }); + + describe('utiq messageHandler', () => { + afterEach(() => { + storage.removeDataFromLocalStorage(utiqPassKey); + }); + + const domains = [ + 'domain1', + 'domain2', + 'domain3', + ]; + + domains.forEach(domain => { + it(`correctly sets utiq value for domain name ${domain}`, (done) => { + const idGraph = { + 'domain': domain, + 'mtid': 'mtidValue', + }; + + storage.setDataInLocalStorage(utiqPassKey, JSON.stringify(getStorageData(idGraph))); + + const eventData = { + data: `{\"msgType\":\"MNOSELECTOR\",\"body\":{\"url\":\"https://${domain}/some/path\"}}` + }; + + window.dispatchEvent(new MessageEvent('message', eventData)); + + const response = utiqMtpIdSubmodule.getId(); + expect(response).to.have.property('id'); + expect(response.id).to.have.property('utiqMtp'); + expect(response.id.utiqMtp).to.be.equal('mtidValue'); + done(); + }); + }); + }); +});