Skip to content

Verizon Media user id module #5786

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

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 = 'verizonMediaId';
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',
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);
31 changes: 31 additions & 0 deletions modules/verizonMediaSystemId.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## 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: {
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.he | Required | String | The SHA-256 hashed user email address | `"529cb86de31e9547a712d9f380146e98bbd39beec"` |
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('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(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;
});
});
});
});