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 @@ -15,7 +15,8 @@
"intentIqIdSystem",
"zeotapIdPlusIdSystem",
"haloIdSystem",
"quantcastIdSystem"
"quantcastIdSystem",
"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 @@ -142,6 +142,12 @@ const USER_IDS_CONFIG = {
'quantcastId': {
source: 'quantcast.com',
atype: 1
},

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

Expand Down
12 changes: 12 additions & 0 deletions modules/userId/eids.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ userIdAsEids = [
atype: 1
}]
},

{
source: 'sharedid.org',
uids: [{
Expand All @@ -96,26 +97,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
}]
}
]
```
89 changes: 89 additions & 0 deletions modules/verizonMediaIdSystem.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/**
* 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 = 'verizonMedia';
smenzer marked this conversation as resolved.
Show resolved Hide resolved
const VMUID_ENDPOINT = 'https://ups.analytics.yahoo.com/ups/58300/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 {SubmoduleParams} [configParams]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param {SubmoduleParams} [configParams]
* @param {SubmoduleConfig} [config]

* @param {ConsentData} [consentData]
* @returns {IdResponse|undefined}
*/
getId(configParams, consentData) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
getId(configParams, consentData) {
getId(config, consentData) {
const configParams = (config && config.params) || {};

if (!configParams || typeof configParams.he !== 'string') {
utils.logError('The verizonMediaId submodule requires the \'he\' parameter to be defined.');
return;
}

const data = {
'1p': [1, '1', true].includes(configParams['1p']) ? '1' : '0',
smenzer marked this conversation as resolved.
Show resolved Hide resolved
he: configParams.he,
gdpr: isEUConsentRequired(consentData) ? '1' : '0',
euconsent: isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '',
us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : ''
};

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();
}
};
let url = `${configParams.endpoint || VMUID_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);
35 changes: 35 additions & 0 deletions modules/verizonMediaSystemId.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
## Verizon Media User ID Submodule

Verizon Media User ID Module.

### Prebid Params

```
pbjs.setConfig({
userSync: {
userIds: [{
name: 'verizonMedia',
smenzer marked this conversation as resolved.
Show resolved Hide resolved
storage: {
name: 'vmuid',
type: 'cookie',
expires: 30
},
params: {
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 - `"verizonMedia"` | `"verizonMedia"` |
smenzer marked this conversation as resolved.
Show resolved Hide resolved
| params | Required | Object | Data for Verizon Media ID initialization. | |
| params.he | Required | String | The SHA-256 hashed user email address | `"529cb86de31e9547a712d9f380146e98bbd39beec"` |
| storage | Required | Object | The publisher must specify the local storage in which to store the results of the call to get the user ID. This can be either cookie or HTML5 storage. | |
| storage.type | Required | String | This is where the results of the user ID will be stored. The recommended method is `localStorage` by specifying `html5`. | `"html5"` |
smenzer marked this conversation as resolved.
Show resolved Hide resolved
| storage.name | Required | String | The name of the cookie or html5 local storage where the user ID will be stored. | `"vmuid"` |
| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. | `365` |
152 changes: 152 additions & 0 deletions test/spec/modules/verizonMediaIdSystem_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
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 PROD_ENDPOINT = 'https://ups.analytics.yahoo.com/ups/58300/fed';
const OVERRIDE_ENDPOINT = 'https://foo/bar';

it('should have the correct module name declared', () => {
expect(verizonMediaIdSubmodule.name).to.equal('verizonMedia');
smenzer marked this conversation as resolved.
Show resolved Hide resolved
});

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(configParams, consentData);
result.callback(sinon.stub());
return result;
}

it('returns undefined if the hashed email address is not passed', () => {
expect(verizonMediaIdSubmodule.getId({}, 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 = verizonMediaIdSubmodule.getId({
he: HASHED_EMAIL
}, 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,
}, 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(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
}, 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
}, 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
}, 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
}, 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;
});
});
});
});