Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verizon Media user id module #5786

Merged
merged 13 commits into from
Oct 21, 2020
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
}]
}
]
```
97 changes: 97 additions & 0 deletions modules/verizonMediaIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* 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 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,
/**
* 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"` |
178 changes: 178 additions & 0 deletions test/spec/modules/verizonMediaIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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');
});

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;
});
});
});
});