Skip to content

Commit

Permalink
Verizon Media user id module (#5786)
Browse files Browse the repository at this point in the history
* 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 <sam@slimkrazy.com>
  • Loading branch information
slimkrazy and slimkrazy authored Oct 21, 2020
1 parent 01eb953 commit 3423e7b
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 1 deletion.
3 changes: 2 additions & 1 deletion modules/.submodules.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"haloIdSystem",
"quantcastIdSystem",
"idxIdSystem",
"fabrickIdSystem"
"fabrickIdSystem",
"verizonMediaIdSystem"
],
"adpod": [
"freeWheelAdserverVideo",
Expand Down
6 changes: 6 additions & 0 deletions modules/userId/eids.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,12 @@ const USER_IDS_CONFIG = {
source: 'idx.lat',
atype: 1
},

// Verizon Media
'vmuid': {
source: 'verizonmedia.com',
atype: 1
}
};

// this function will create an eid object for the given UserId sub-module
Expand Down
12 changes: 12 additions & 0 deletions modules/userId/eids.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ userIdAsEids = [
atype: 1
}]
},
{
source: 'sharedid.org',
uids: [{
Expand All @@ -100,26 +101,37 @@ userIdAsEids = [
}
}]
},
{
source: 'zeotap.com',
uids: [{
id: 'some-random-id-value',
atype: 1
}]
},
{
source: 'audigent.com',
uids: [{
id: 'some-random-id-value',
atype: 1
}]
},
{
source: 'quantcast.com',
uids: [{
id: 'some-random-id-value',
atype: 1
}]
},
{
source: 'verizonmedia.com',
uids: [{
id: 'some-random-id-value',
atype: 1
}]
}
]
```
103 changes: 103 additions & 0 deletions modules/verizonMediaIdSystem.js
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);
33 changes: 33 additions & 0 deletions modules/verizonMediaSystemId.md
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"` |
182 changes: 182 additions & 0 deletions test/spec/modules/verizonMediaIdSystem_spec.js
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;
});
});
});
});

0 comments on commit 3423e7b

Please sign in to comment.