diff --git a/modules/growthCodeIdSystem.js b/modules/growthCodeIdSystem.js new file mode 100644 index 00000000000..68fe2c7e925 --- /dev/null +++ b/modules/growthCodeIdSystem.js @@ -0,0 +1,164 @@ +/** + * This module adds GrowthCodeId to the User ID module + * The {@link module:modules/userId} module is required + * @module modules/growthCodeIdSystem + * @requires module:modules/userId + */ + +import {logError, logInfo, tryAppendQueryString} from '../src/utils.js'; +import {ajax} from '../src/ajax.js'; +import { submodule } from '../src/hook.js' +import { getStorageManager } from '../src/storageManager.js'; + +const GCID_EXPIRY = 45; +const MODULE_NAME = 'growthCodeId'; +const GC_DATA_KEY = '_gc_data'; +const ENDPOINT_URL = 'https://p2.gcprivacy.com/v1/pb?' + +export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); + +/** + * Read GrowthCode data from cookie or local storage + * @param key + * @return {string} + */ +export function readData(key) { + try { + if (storage.hasLocalStorage()) { + return storage.getDataFromLocalStorage(key); + } + if (storage.cookiesAreEnabled()) { + return storage.getCookie(key); + } + } catch (error) { + logError(error); + } +} + +/** + * Store GrowthCode data in either cookie or local storage + * expiration date: 45 days + * @param key + * @param {string} value + */ +function storeData(key, value) { + try { + logInfo(MODULE_NAME + ': storing data: key=' + key + ' value=' + value); + + if (value) { + if (storage.hasLocalStorage()) { + storage.setDataInLocalStorage(key, value); + } + const expiresStr = (new Date(Date.now() + (GCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); + if (storage.cookiesAreEnabled()) { + storage.setCookie(key, value, expiresStr, 'LAX'); + } + } + } catch (error) { + logError(error); + } +} + +/** + * Parse json if possible, else return null + * @param data + * @param {object|null} + */ +function tryParse(data) { + try { + return JSON.parse(data); + } catch (err) { + logError(err); + return null; + } +} + +/** @type {Submodule} */ +export const growthCodeIdSubmodule = { + /** + * used to link submodule with config + * @type {string} + */ + name: MODULE_NAME, + /** + * decode the stored id value for passing to bid requests + * @function + * @param {{string}} value + * @returns {{growthCodeId: {string}}|undefined} + */ + decode(value) { + return value && value !== '' ? { 'growthCodeId': value } : undefined; + }, + /** + * performs action to obtain id and return a value in the callback's response argument + * @function + * @param {SubmoduleConfig} [config] + * @returns {IdResponse|undefined} + */ + getId(config, consentData) { + const configParams = (config && config.params) || {}; + if (!configParams || typeof configParams.pid !== 'string') { + logError('User ID - GrowthCodeID submodule requires a valid Partner ID to be defined'); + return; + } + + const gdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0; + const consentString = gdpr ? consentData.consentString : ''; + if (gdpr && !consentString) { + logInfo('Consent string is required to call GrowthCode id.'); + return; + } + + let publisherId = configParams.publisher_id ? configParams.publisher_id : '_sharedID'; + + let sharedId; + if (configParams.publisher_id_storage === 'html5') { + sharedId = storage.getDataFromLocalStorage(publisherId, null) ? (storage.getDataFromLocalStorage(publisherId, null)) : null; + } else { + sharedId = storage.getCookie(publisherId, null) ? (storage.getCookie(publisherId, null)) : null; + } + if (!sharedId) { + logError('User ID - Publisher ID is not correctly setup.'); + } + + const resp = function(callback) { + let gcData = tryParse(readData(GC_DATA_KEY)); + if (gcData) { + callback(gcData); + } else { + let segment = window.location.pathname.substr(1).replace(/\/+$/, ''); + if (segment === '') { + segment = 'home'; + } + + let url = configParams.url ? configParams.url : ENDPOINT_URL; + url = tryAppendQueryString(url, 'pid', configParams.pid); + url = tryAppendQueryString(url, 'uid', sharedId); + url = tryAppendQueryString(url, 'u', window.location.href); + url = tryAppendQueryString(url, 'h', window.location.hostname); + url = tryAppendQueryString(url, 's', segment); + url = tryAppendQueryString(url, 'r', document.referrer); + + ajax(url, { + success: response => { + let respJson = tryParse(response); + // If response is a valid json and should save is true + if (respJson) { + storeData(GC_DATA_KEY, JSON.stringify(respJson)) + callback(respJson); + } else { + callback(); + } + }, + error: error => { + logError(MODULE_NAME + ': ID fetch encountered an error', error); + callback(); + } + }, undefined, {method: 'GET', withCredentials: true}) + } + }; + return { callback: resp }; + } +}; + +submodule('userId', growthCodeIdSubmodule); diff --git a/modules/growthCodeIdSystem.md b/modules/growthCodeIdSystem.md new file mode 100644 index 00000000000..f804686a7a9 --- /dev/null +++ b/modules/growthCodeIdSystem.md @@ -0,0 +1,37 @@ +## GrowthCode User ID Submodule + +GrowthCode provides Id Enrichment for requests. + +## Building Prebid with GrowthCode Support + +First, make sure to add the GrowthCode submodule to your Prebid.js package with: + +``` +gulp build --modules=growthCodeIdSystem,userId +``` + +The following configuration parameters are available: + +```javascript +pbjs.setConfig({ + userSync: { + userIds: [{ + name: 'growthCodeId', + params: { + pid: 'TEST01', // Set your Partner ID here for production (obtained from Growthcode) + publisher_id: '_sharedID', + publisher_id_storage: 'html5' + } + }] + } +}); +``` + +| Param under userSync.userIds[] | Scope | Type | Description | Example | +|--------------------------------|----------|--------| --- |-----------------| +| name | Required | String | The name of this module. | `"growthCodeId"` | +| params | Required | Object | Details of module params. | | +| params.pid | Required | String | This is the Parter ID value obtained from GrowthCode | `"TEST01"` | +| params.url | Optional | String | Custom URL for server | | +| params.publisher_id | Optional | String | Name if the variable that holds your publisher ID | `"_sharedID"` | +| params.publisher_id_storage | Optional | String | Publisher ID storage (cookie, html5) | `"html5"` | diff --git a/modules/userId/eids.js b/modules/userId/eids.js index 689916f6a3e..23dc9809041 100644 --- a/modules/userId/eids.js +++ b/modules/userId/eids.js @@ -5,6 +5,25 @@ export const USER_IDS_CONFIG = { // key-name : {config} + // GrowthCode + 'growthCodeId': { + getValue: function(data) { + return data.gc_id + }, + source: 'growthcode.io', + atype: 1, + getUidExt: function(data) { + const extendedData = pick(data, [ + 'h1', + 'h2', + 'h3', + ]); + if (Object.keys(extendedData).length) { + return extendedData; + } + } + }, + // trustpid 'trustpid': { source: 'trustpid.com', diff --git a/test/spec/modules/growthCodeIdSystem_spec.js b/test/spec/modules/growthCodeIdSystem_spec.js new file mode 100644 index 00000000000..dce995d25e0 --- /dev/null +++ b/test/spec/modules/growthCodeIdSystem_spec.js @@ -0,0 +1,83 @@ +import { growthCodeIdSubmodule } from 'modules/growthCodeIdSystem.js'; +import * as utils from 'src/utils.js'; +import { server } from 'test/mocks/xhr.js'; +import { uspDataHandler } from 'src/adapterManager.js'; +import {expect} from 'chai'; +import {getStorageManager} from '../../../src/storageManager.js'; + +const GCID_EXPIRY = 45; +const MODULE_NAME = 'growthCodeId'; +const SHAREDID = 'fe9c5c89-7d56-4666-976d-e07e73b3b664'; + +export const storage = getStorageManager({ gvlid: undefined, moduleName: MODULE_NAME }); + +const getIdParams = {params: { + pid: 'TEST01', + publisher_id: '_sharedid', + publisher_id_storage: 'html5', +}}; + +describe('growthCodeIdSystem', () => { + let logErrorStub; + + beforeEach(function () { + logErrorStub = sinon.stub(utils, 'logError'); + storage.setDataInLocalStorage('_sharedid', SHAREDID); + const expiresStr = (new Date(Date.now() + (GCID_EXPIRY * (60 * 60 * 24 * 1000)))).toUTCString(); + if (storage.cookiesAreEnabled()) { + storage.setCookie('_sharedid', SHAREDID, expiresStr, 'LAX'); + } + }); + + afterEach(function () { + logErrorStub.restore(); + }); + + describe('name', () => { + it('should expose the name of the submodule', () => { + expect(growthCodeIdSubmodule.name).to.equal('growthCodeId'); + }); + }); + + it('should NOT call the growthcode id endpoint if gdpr applies but consent string is missing', function () { + let submoduleCallback = growthCodeIdSubmodule.getId(getIdParams, { gdprApplies: true }, undefined); + expect(submoduleCallback).to.be.undefined; + }); + + it('should log an error if pid configParam was not passed when getId', function () { + growthCodeIdSubmodule.getId(); + expect(logErrorStub.callCount).to.be.equal(1); + }); + + it('should log an error if sharedId (LocalStore) is not setup correctly', function () { + growthCodeIdSubmodule.getId({params: { + pid: 'TEST01', + publisher_id: '_sharedid_bad', + publisher_id_storage: 'html5', + }}); + expect(logErrorStub.callCount).to.be.equal(1); + }); + + it('should log an error if sharedId (LocalStore) is not setup correctly', function () { + growthCodeIdSubmodule.getId({params: { + pid: 'TEST01', + publisher_id: '_sharedid_bad', + publisher_id_storage: 'cookie', + }}); + expect(logErrorStub.callCount).to.be.equal(1); + }); + + it('should call the growthcode id endpoint', function () { + let callBackSpy = sinon.spy(); + let submoduleCallback = growthCodeIdSubmodule.getId(getIdParams).callback; + submoduleCallback(callBackSpy); + let request = server.requests[0]; + expect(request.url.substr(0, 85)).to.be.eq('https://p2.gcprivacy.com/v1/pb?pid=TEST01&uid=' + SHAREDID + '&u='); + request.respond( + 200, + {}, + JSON.stringify({}) + ); + expect(callBackSpy.calledOnce).to.be.true; + }); +})