diff --git a/modules/.submodules.json b/modules/.submodules.json index f08db109bb6..e4de1819a16 100644 --- a/modules/.submodules.json +++ b/modules/.submodules.json @@ -32,7 +32,8 @@ "flocIdSystem", "amxIdSystem", "naveggId", - "imuIdSystem" + "imuIdSystem", + "publinkIdSystem" ], "adpod": [ "freeWheelAdserverVideo", diff --git a/modules/publinkIdSystem.js b/modules/publinkIdSystem.js new file mode 100644 index 00000000000..5e549732097 --- /dev/null +++ b/modules/publinkIdSystem.js @@ -0,0 +1,123 @@ +/** + * This module adds the PublinkId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/publinkIdSystem + * @requires module:modules/userId + */ + +import {submodule} from '../src/hook.js'; +import {getStorageManager} from '../src/storageManager.js'; +import {ajax} from '../src/ajax.js'; +import * as utils from '../src/utils.js'; +import {uspDataHandler} from '../src/adapterManager.js'; + +const MODULE_NAME = 'publinkId'; +const GVLID = 24; +const PUBLINK_COOKIE = '_publink'; +const PUBLINK_S2S_COOKIE = '_publink_srv'; + +export const storage = getStorageManager(GVLID); + +function publinkIdUrl(params, consentData) { + let url = utils.parseUrl('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink'); + url.search = { + deh: params.e, + mpn: 'Prebid.js', + mpv: '$prebid.version$', + }; + if (consentData) { + url.search.gdpr = (consentData.gdprApplies) ? 1 : 0; + url.search.gdpr_consent = consentData.consentString; + } + + const usPrivacyString = uspDataHandler.getConsentData(); + if (usPrivacyString && typeof usPrivacyString === 'string') { + url.search.us_privacy = usPrivacyString; + } + + return utils.buildUrl(url); +} + +function makeCallback(config = {}, consentData) { + return function(prebidCallback) { + const options = {method: 'GET', withCredentials: true}; + let handleResponse = function(responseText, xhr) { + if (xhr.status === 200) { + let response = JSON.parse(responseText); + if (response) { + prebidCallback(response.publink); + } + } + }; + if (config.params && config.params.e) { + ajax(publinkIdUrl(config.params, consentData), handleResponse, undefined, options); + } + }; +} + +function getlocalValue() { + let result; + function getData(key) { + let value; + if (storage.hasLocalStorage()) { + value = storage.getDataFromLocalStorage(key); + } + if (!value) { + value = storage.getCookie(key); + } + + if (typeof value === 'string') { + try { + const obj = JSON.parse(value); + if (obj && obj.exp && obj.exp > Date.now()) { + return obj.publink; + } + } catch (e) { + utils.logError(e); + } + } + } + result = getData(PUBLINK_S2S_COOKIE); + if (!result) { + result = getData(PUBLINK_COOKIE); + } + return result; +} + +/** @type {Submodule} */ +export const publinkIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + gvlid: GVLID, + + /** + * decode the stored id value for passing to bid requests + * @function + * @param {string} id encrypted userid + * @returns {{publinkId: string} | undefined} + */ + decode(publinkId) { + return {publink: publinkId}; + }, + + /** + * performs action to obtain id + * Use a publink cookie first if it is present, otherwise use prebids copy, if neither are available callout to get a new id + * @function + * @param {SubmoduleConfig} [config] Config object with params and storage properties + * @returns {IdResponse} + */ + getId: function(config, consentData, storedId) { + const localValue = getlocalValue(); + if (localValue) { + return {id: localValue}; + } + if (!storedId) { + return {callback: makeCallback(config, consentData)}; + } + } +}; +submodule('userId', publinkIdSubmodule); diff --git a/modules/publinkIdSystem.md b/modules/publinkIdSystem.md new file mode 100644 index 00000000000..669828322a5 --- /dev/null +++ b/modules/publinkIdSystem.md @@ -0,0 +1,28 @@ +## Publink User ID Submodule + +Publink user id module + +## Configuration Descriptions for the `userId` Configuration Section + +| Param Name | Required | Type | Description | Example | +| --- | --- | --- | --- | --- | +| name | Yes | String | module identifier | `"publinkId"` | +| params.e | Yes | String | hashed email address | `"e80b5017098950fc58aad83c8c14978e"` | + +### Example configuration for Publink +``` +pbjs.setConfig({ + userSync: { + userIds: [{ + name: "publinkId", + storage: { + name: "pbjs_publink", + type: "html5" + }, + params: { + e: "e80b5017098950fc58aad83c8c14978e", // example hashed email (md5) + } + }], + } + }); +``` diff --git a/modules/userId/eids.js b/modules/userId/eids.js index f6707f211d2..0a8e94883b4 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -228,6 +228,10 @@ const USER_IDS_CONFIG = { source: 'amxrtb.com', atype: 1, }, + 'publinkId': { + source: 'epsilon.com', + atype: 3 + }, 'kpuid': { source: 'kpuid.com', atype: 3 diff --git a/test/spec/modules/publinkIdSystem_spec.js b/test/spec/modules/publinkIdSystem_spec.js new file mode 100644 index 00000000000..326285709f3 --- /dev/null +++ b/test/spec/modules/publinkIdSystem_spec.js @@ -0,0 +1,136 @@ +import {publinkIdSubmodule} from 'modules/publinkIdSystem.js'; +import {getStorageManager} from '../../../src/storageManager'; +import {server} from 'test/mocks/xhr.js'; +import sinon from 'sinon'; +import {uspDataHandler} from '../../../src/adapterManager'; + +export const storage = getStorageManager(24); +const TEST_COOKIE_VALUE = 'cookievalue'; +describe('PublinkIdSystem', () => { + describe('decode', () => { + it('decode', () => { + const result = publinkIdSubmodule.decode(TEST_COOKIE_VALUE); + expect(result).deep.equals({publink: TEST_COOKIE_VALUE}); + }); + }); + + describe('Fetch Local Cookies', () => { + const PUBLINK_COOKIE = '_publink'; + const PUBLINK_SRV_COOKIE = '_publink_srv'; + const EXP = Date.now() + 60 * 60 * 24 * 7 * 1000; + const COOKIE_VALUE = {publink: 'publinkCookieValue', exp: EXP}; + const LOCAL_VALUE = {publink: 'publinkLocalStorageValue', exp: EXP}; + const COOKIE_EXPIRATION = (new Date(Date.now() + 60 * 60 * 24 * 1000)).toUTCString(); + const DELETE_COOKIE = 'Thu, 01 Jan 1970 00:00:01 GMT'; + it('publink srv cookie', () => { + storage.setCookie(PUBLINK_SRV_COOKIE, JSON.stringify(COOKIE_VALUE), COOKIE_EXPIRATION); + const result = publinkIdSubmodule.getId(); + expect(result.id).to.equal(COOKIE_VALUE.publink); + storage.setCookie(PUBLINK_SRV_COOKIE, '', DELETE_COOKIE); + }); + it('publink srv local storage', () => { + storage.setDataInLocalStorage(PUBLINK_SRV_COOKIE, JSON.stringify(LOCAL_VALUE)); + const result = publinkIdSubmodule.getId(); + expect(result.id).to.equal(LOCAL_VALUE.publink); + storage.removeDataFromLocalStorage(PUBLINK_SRV_COOKIE); + }); + it('publink cookie', () => { + storage.setCookie(PUBLINK_COOKIE, JSON.stringify(COOKIE_VALUE), COOKIE_EXPIRATION); + const result = publinkIdSubmodule.getId(); + expect(result.id).to.equal(COOKIE_VALUE.publink); + storage.setCookie(PUBLINK_COOKIE, '', DELETE_COOKIE); + }); + it('publink local storage', () => { + storage.setDataInLocalStorage(PUBLINK_COOKIE, JSON.stringify(LOCAL_VALUE)); + const result = publinkIdSubmodule.getId(); + expect(result.id).to.equal(LOCAL_VALUE.publink); + storage.removeDataFromLocalStorage(PUBLINK_COOKIE); + }); + it('ignore expired cookie', () => { + storage.setDataInLocalStorage(PUBLINK_COOKIE, JSON.stringify({publink: 'value', exp: Date.now() - 60 * 60 * 24 * 1000})); + const result = publinkIdSubmodule.getId(); + expect(result.id).to.be.undefined; + storage.removeDataFromLocalStorage(PUBLINK_COOKIE); + }); + it('priority goes to publink_srv cookie', () => { + storage.setCookie(PUBLINK_SRV_COOKIE, JSON.stringify(COOKIE_VALUE), COOKIE_EXPIRATION); + storage.setDataInLocalStorage(PUBLINK_COOKIE, JSON.stringify(LOCAL_VALUE)); + const result = publinkIdSubmodule.getId(); + expect(result.id).to.equal(COOKIE_VALUE.publink); + storage.setCookie(PUBLINK_SRV_COOKIE, '', DELETE_COOKIE); + storage.removeDataFromLocalStorage(PUBLINK_COOKIE); + }); + }); + + describe('getId', () => { + const serverResponse = {publink: 'ec0xHT3yfAOnykP64Qf0ORSi7LjNT1wju04ZSCsoPBekOJdBwK-0Zl_lXKDNnzhauC4iszBc-PvA1Be6IMlh1QocA'}; + it('no config', () => { + const result = publinkIdSubmodule.getId(); + expect(result).to.exist; + expect(result.callback).to.be.a('function'); + }); + it('Use local copy', () => { + const result = publinkIdSubmodule.getId({}, undefined, TEST_COOKIE_VALUE); + expect(result).to.be.undefined; + }); + + describe('callout for id', () => { + let callbackSpy = sinon.spy(); + + beforeEach(() => { + callbackSpy.resetHistory(); + }); + + it('Fetch with consent data', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'hashedemailvalue'}}; + const consentData = {gdprApplies: 1, consentString: 'myconsentstring'}; + let submoduleCallback = publinkIdSubmodule.getId(config, consentData).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink?deh=hashedemailvalue&mpn=Prebid.js&mpv=$prebid.version$&gdpr=1&gdpr_consent=myconsentstring'); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + + it('server doesnt respond', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'hashedemailvalue'}}; + let submoduleCallback = publinkIdSubmodule.getId(config).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink?deh=hashedemailvalue&mpn=Prebid.js&mpv=$prebid.version$'); + + request.respond(204, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.false; + }); + }); + + describe('usPrivacy', () => { + let callbackSpy = sinon.spy(); + const oldPrivacy = uspDataHandler.getConsentData(); + before(() => { + uspDataHandler.setConsentData('1YNN'); + }); + after(() => { + uspDataHandler.setConsentData(oldPrivacy); + callbackSpy.resetHistory(); + }); + + it('Fetch with usprivacy data', () => { + const config = {storage: {type: 'cookie'}, params: {e: 'hashedemailvalue'}}; + let submoduleCallback = publinkIdSubmodule.getId(config).callback; + submoduleCallback(callbackSpy); + + let request = server.requests[0]; + expect(request.url).to.equal('https://proc.ad.cpe.dotomi.com/cvx/client/sync/publink?deh=hashedemailvalue&mpn=Prebid.js&mpv=$prebid.version$&us_privacy=1YNN'); + + request.respond(200, {}, JSON.stringify(serverResponse)); + expect(callbackSpy.calledOnce).to.be.true; + expect(callbackSpy.lastCall.lastArg).to.equal(serverResponse.publink); + }); + }); + }); +});