-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Verizon Media user id module (#5786)
* Initial work on Verizon Media User ID module * Submodule tests * Add sample eid object for Verizon Media * Documentation update * Switch to HTTP GET, update tests. * Remove single test restriction. * Documentation update * Addressing initial PR feedback. * Accept pixelId parameter to construct VMUID URL * Fix tests following API signature change * Add IAB vendor ID Co-authored-by: slimkrazy <[email protected]>
- Loading branch information
Showing
6 changed files
with
338 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
/** | ||
* This module adds verizonMediaId to the User ID module | ||
* The {@link module:modules/userId} module is required | ||
* @module modules/verizonMediaIdSystem | ||
* @requires module:modules/userId | ||
*/ | ||
|
||
import {ajax} from '../src/ajax.js'; | ||
import {submodule} from '../src/hook.js'; | ||
import * as utils from '../src/utils.js'; | ||
|
||
const MODULE_NAME = 'verizonMediaId'; | ||
const VENDOR_ID = 25; | ||
const PLACEHOLDER = '__PIXEL_ID__'; | ||
const VMUID_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`; | ||
|
||
function isEUConsentRequired(consentData) { | ||
return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies); | ||
} | ||
|
||
/** @type {Submodule} */ | ||
export const verizonMediaIdSubmodule = { | ||
/** | ||
* used to link submodule with config | ||
* @type {string} | ||
*/ | ||
name: MODULE_NAME, | ||
/** | ||
* Vendor id of Verizon Media EMEA Limited | ||
* @type {Number} | ||
*/ | ||
gvlid: VENDOR_ID, | ||
/** | ||
* decode the stored id value for passing to bid requests | ||
* @function | ||
* @returns {{vmuid: string} | undefined} | ||
*/ | ||
decode(value) { | ||
return (value && typeof value.vmuid === 'string') ? {vmuid: value.vmuid} : undefined; | ||
}, | ||
/** | ||
* get the VerizonMedia Id | ||
* @function | ||
* @param {SubmoduleConfig} [config] | ||
* @param {ConsentData} [consentData] | ||
* @returns {IdResponse|undefined} | ||
*/ | ||
getId(config, consentData) { | ||
const params = config.params || {}; | ||
if (!params || typeof params.he !== 'string' || | ||
(typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) { | ||
utils.logError('The verizonMediaId submodule requires the \'he\' and \'pixelId\' parameters to be defined.'); | ||
return; | ||
} | ||
|
||
const data = { | ||
'1p': [1, '1', true].includes(params['1p']) ? '1' : '0', | ||
he: params.he, | ||
gdpr: isEUConsentRequired(consentData) ? '1' : '0', | ||
euconsent: isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '', | ||
us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : '' | ||
}; | ||
|
||
if (params.pixelId) { | ||
data.pixelId = params.pixelId | ||
} | ||
|
||
const resp = function (callback) { | ||
const callbacks = { | ||
success: response => { | ||
let responseObj; | ||
if (response) { | ||
try { | ||
responseObj = JSON.parse(response); | ||
} catch (error) { | ||
utils.logError(error); | ||
} | ||
} | ||
callback(responseObj); | ||
}, | ||
error: error => { | ||
utils.logError(`${MODULE_NAME}: ID fetch encountered an error`, error); | ||
callback(); | ||
} | ||
}; | ||
const endpoint = VMUID_ENDPOINT.replace(PLACEHOLDER, params.pixelId); | ||
let url = `${params.endpoint || endpoint}?${utils.formatQS(data)}`; | ||
verizonMediaIdSubmodule.getAjaxFn()(url, callbacks, null, {method: 'GET', withCredentials: true}); | ||
}; | ||
return {callback: resp}; | ||
}, | ||
|
||
/** | ||
* Return the function used to perform XHR calls. | ||
* Utilised for each of testing. | ||
* @returns {Function} | ||
*/ | ||
getAjaxFn() { | ||
return ajax; | ||
} | ||
}; | ||
|
||
submodule('userId', verizonMediaIdSubmodule); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
## Verizon Media User ID Submodule | ||
|
||
Verizon Media User ID Module. | ||
|
||
### Prebid Params | ||
|
||
``` | ||
pbjs.setConfig({ | ||
userSync: { | ||
userIds: [{ | ||
name: 'verizonMediaId', | ||
storage: { | ||
name: 'vmuid', | ||
type: 'html5', | ||
expires: 30 | ||
}, | ||
params: { | ||
pixelId: 58776, | ||
he: '0bef996248d63cea1529cb86de31e9547a712d9f380146e98bbd39beec70355a' | ||
} | ||
}] | ||
} | ||
}); | ||
``` | ||
## Parameter Descriptions for the `usersync` Configuration Section | ||
The below parameters apply only to the Verizon Media User ID Module integration. | ||
|
||
| Param under usersync.userIds[] | Scope | Type | Description | Example | | ||
| --- | --- | --- | --- | --- | | ||
| name | Required | String | ID value for the Verizon Media module - `"verizonMediaId"` | `"verizonMediaId"` | | ||
| params | Required | Object | Data for Verizon Media ID initialization. | | | ||
| params.pixelId | Required | Number | The Verizon Media supplied publisher specific pixel Id | `8976` | | ||
| params.he | Required | String | The SHA-256 hashed user email address | `"529cb86de31e9547a712d9f380146e98bbd39beec"` | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,182 @@ | ||
import {expect} from 'chai'; | ||
import * as utils from 'src/utils.js'; | ||
import {verizonMediaIdSubmodule} from 'modules/verizonMediaIdSystem.js'; | ||
|
||
describe('Verizon Media ID Submodule', () => { | ||
const HASHED_EMAIL = '6bda6f2fa268bf0438b5423a9861a2cedaa5dec163c03f743cfe05c08a8397b2'; | ||
const PIXEL_ID = '1234'; | ||
const PROD_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PIXEL_ID}/fed`; | ||
const OVERRIDE_ENDPOINT = 'https://foo/bar'; | ||
|
||
it('should have the correct module name declared', () => { | ||
expect(verizonMediaIdSubmodule.name).to.equal('verizonMediaId'); | ||
}); | ||
|
||
it('should have the correct TCFv2 Vendor ID declared', () => { | ||
expect(verizonMediaIdSubmodule.gvlid).to.equal(25); | ||
}); | ||
|
||
describe('getId()', () => { | ||
let ajaxStub; | ||
let getAjaxFnStub; | ||
let consentData; | ||
beforeEach(() => { | ||
ajaxStub = sinon.stub(); | ||
getAjaxFnStub = sinon.stub(verizonMediaIdSubmodule, 'getAjaxFn'); | ||
getAjaxFnStub.returns(ajaxStub); | ||
|
||
consentData = { | ||
gdpr: { | ||
gdprApplies: 1, | ||
consentString: 'GDPR_CONSENT_STRING' | ||
}, | ||
uspConsent: 'USP_CONSENT_STRING' | ||
}; | ||
}); | ||
|
||
afterEach(() => { | ||
getAjaxFnStub.restore(); | ||
}); | ||
|
||
function invokeGetIdAPI(configParams, consentData) { | ||
let result = verizonMediaIdSubmodule.getId({ | ||
params: configParams | ||
}, consentData); | ||
if (typeof result === 'object') { | ||
result.callback(sinon.stub()); | ||
} | ||
return result; | ||
} | ||
|
||
it('returns undefined if he and pixelId params are not passed', () => { | ||
expect(invokeGetIdAPI({}, consentData)).to.be.undefined; | ||
expect(ajaxStub.callCount).to.equal(0); | ||
}); | ||
|
||
it('returns undefined if the pixelId param is not passed', () => { | ||
expect(invokeGetIdAPI({ | ||
he: HASHED_EMAIL | ||
}, consentData)).to.be.undefined; | ||
expect(ajaxStub.callCount).to.equal(0); | ||
}); | ||
|
||
it('returns undefined if the he param is not passed', () => { | ||
expect(invokeGetIdAPI({ | ||
pixelId: PIXEL_ID | ||
}, consentData)).to.be.undefined; | ||
expect(ajaxStub.callCount).to.equal(0); | ||
}); | ||
|
||
it('returns an object with the callback function if the correct params are passed', () => { | ||
let result = invokeGetIdAPI({ | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID | ||
}, consentData); | ||
expect(result).to.be.an('object').that.has.all.keys('callback'); | ||
expect(result.callback).to.be.a('function'); | ||
}); | ||
|
||
it('Makes an ajax GET request to the production API endpoint with query params', () => { | ||
invokeGetIdAPI({ | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID | ||
}, consentData); | ||
|
||
const expectedParams = { | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID, | ||
'1p': '0', | ||
gdpr: '1', | ||
euconsent: consentData.gdpr.consentString, | ||
us_privacy: consentData.uspConsent | ||
}; | ||
const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); | ||
|
||
expect(ajaxStub.firstCall.args[0].indexOf(`${PROD_ENDPOINT}?`)).to.equal(0); | ||
expect(requestQueryParams).to.deep.equal(expectedParams); | ||
expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); | ||
}); | ||
|
||
it('Makes an ajax GET request to the specified override API endpoint with query params', () => { | ||
invokeGetIdAPI({ | ||
he: HASHED_EMAIL, | ||
endpoint: OVERRIDE_ENDPOINT | ||
}, consentData); | ||
|
||
const expectedParams = { | ||
he: HASHED_EMAIL, | ||
'1p': '0', | ||
gdpr: '1', | ||
euconsent: consentData.gdpr.consentString, | ||
us_privacy: consentData.uspConsent | ||
}; | ||
const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); | ||
|
||
expect(ajaxStub.firstCall.args[0].indexOf(`${OVERRIDE_ENDPOINT}?`)).to.equal(0); | ||
expect(requestQueryParams).to.deep.equal(expectedParams); | ||
expect(ajaxStub.firstCall.args[3]).to.deep.equal({method: 'GET', withCredentials: true}); | ||
}); | ||
|
||
it('sets the callbacks param of the ajax function call correctly', () => { | ||
invokeGetIdAPI({ | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID, | ||
}, consentData); | ||
|
||
expect(ajaxStub.firstCall.args[1]).to.be.an('object').that.has.all.keys(['success', 'error']); | ||
}); | ||
|
||
it('sets GDPR consent data flag correctly when call is under GDPR jurisdiction.', () => { | ||
invokeGetIdAPI({ | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID, | ||
}, consentData); | ||
|
||
const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); | ||
expect(requestQueryParams.gdpr).to.equal('1'); | ||
expect(requestQueryParams.euconsent).to.equal(consentData.gdpr.consentString); | ||
}); | ||
|
||
it('sets GDPR consent data flag correctly when call is NOT under GDPR jurisdiction.', () => { | ||
consentData.gdpr.gdprApplies = false; | ||
|
||
invokeGetIdAPI({ | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID, | ||
}, consentData); | ||
|
||
const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); | ||
expect(requestQueryParams.gdpr).to.equal('0'); | ||
expect(requestQueryParams.euconsent).to.equal(''); | ||
}); | ||
|
||
[1, '1', true].forEach(firstPartyParamValue => { | ||
it(`sets 1p payload property to '1' for a config value of ${firstPartyParamValue}`, () => { | ||
invokeGetIdAPI({ | ||
'1p': firstPartyParamValue, | ||
he: HASHED_EMAIL, | ||
pixelId: PIXEL_ID, | ||
}, consentData); | ||
|
||
const requestQueryParams = utils.parseQS(ajaxStub.firstCall.args[0].split('?')[1]); | ||
expect(requestQueryParams['1p']).to.equal('1'); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('decode()', () => { | ||
const VALID_API_RESPONSE = { | ||
vmuid: '1234' | ||
}; | ||
it('should return a newly constructed object with the vmuid property', () => { | ||
expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.deep.equal(VALID_API_RESPONSE); | ||
expect(verizonMediaIdSubmodule.decode(VALID_API_RESPONSE)).to.not.equal(VALID_API_RESPONSE); | ||
}); | ||
|
||
[{}, '', {foo: 'bar'}].forEach((response) => { | ||
it(`should return undefined for an invalid response "${JSON.stringify(response)}"`, () => { | ||
expect(verizonMediaIdSubmodule.decode(response)).to.be.undefined; | ||
}); | ||
}); | ||
}); | ||
}); |