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

Yahoo ConnectId UserID Module: explicit storage management #9716

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 95 additions & 21 deletions modules/connectIdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,85 @@
import {ajax} from '../src/ajax.js';
import {submodule} from '../src/hook.js';
import {includes} from '../src/polyfill.js';
import {formatQS, logError} from '../src/utils.js';
import {getRefererInfo} from '../src/refererDetection.js';
import {getStorageManager} from '../src/storageManager.js';
import {formatQS, isPlainObject, logError, parseUrl} from '../src/utils.js';
import {uspDataHandler} from '../src/adapterManager.js';

const MODULE_NAME = 'connectId';
const STORAGE_EXPIRY_DAYS = 14;
const VENDOR_ID = 25;
const PLACEHOLDER = '__PIXEL_ID__';
const UPS_ENDPOINT = `https://ups.analytics.yahoo.com/ups/${PLACEHOLDER}/fed`;
const OVERRIDE_OPT_OUT_KEY = 'connectIdOptOut';
const INPUT_PARAM_KEYS = ['pixelId', 'he', 'puid'];
export const storage = getStorageManager({gvlid: VENDOR_ID, moduleName: MODULE_NAME});

/**
* @function
* @param {Object} obj
*/
function storeObject(obj) {
const expires = Date.now() + (60 * 60 * 24 * 1000 * STORAGE_EXPIRY_DAYS);
if (storage.cookiesAreEnabled()) {
setEtldPlusOneCookie(MODULE_NAME, JSON.stringify(obj), new Date(expires), getSiteHostname());
} else if (storage.localStorageIsEnabled()) {
obj.__expires = expires;
storage.setDataInLocalStorage(MODULE_NAME, obj);
}
}

/**
* Attempts to store a cookie on eTLD + 1
*
* @function
* @param {String} key
* @param {String} value
* @param {Date} expirationDate
* @param {String} hostname
*/
function setEtldPlusOneCookie(key, value, expirationDate, hostname) {
const subDomains = hostname.split('.');
for (let i = 0; i < subDomains.length; ++i) {
const domain = subDomains.slice(subDomains.length - i - 1, subDomains.length).join('.');
try {
storage.setCookie(key, value, expirationDate.toUTCString(), null, '.' + domain);
const storedCookie = storage.getCookie(key);
if (storedCookie && storedCookie === value) {
break;
}
} catch (error) {}
}
}

function getIdFromCookie() {
if (storage.cookiesAreEnabled()) {
try {
return JSON.parse(storage.getCookie(MODULE_NAME));
} catch {}
}
return null;
}

function getIdFromLocalStorage() {
if (storage.localStorageIsEnabled()) {
const storedIdData = storage.getDataFromLocalStorage(MODULE_NAME);
if (storedIdData) {
if (isPlainObject(storedIdData) && storedIdData.__expires &&
storedIdData.__expires <= Date.now()) {
storage.removeDataFromLocalStorage(MODULE_NAME);
return null;
}
return storedIdData;
}
}
return null;
}

function getSiteHostname() {
const pageInfo = parseUrl(getRefererInfo().page);
return pageInfo.hostname;
}

/** @type {Submodule} */
export const connectIdSubmodule = {
Expand All @@ -37,8 +108,8 @@ export const connectIdSubmodule = {
if (connectIdSubmodule.userHasOptedOut()) {
return undefined;
}
return (typeof value === 'object' && value.connectid)
? {connectId: value.connectid} : undefined;
return (isPlainObject(value) && (value.connectId || value.connectid))
? {connectId: value.connectId || value.connectid} : undefined;
},
/**
* Gets the Yahoo ConnectID
Expand All @@ -54,21 +125,28 @@ export const connectIdSubmodule = {
const params = config.params || {};
if (!params || (typeof params.he !== 'string' && typeof params.puid !== 'string') ||
(typeof params.pixelId === 'undefined' && typeof params.endpoint === 'undefined')) {
logError('The connectId submodule requires the \'pixelId\' and at least one of the \'he\' ' +
'or \'puid\' parameters to be defined.');
logError(`${MODULE_NAME} module: configurataion requires the 'pixelId' and at ` +
`least one of the 'he' or 'puid' parameters to be defined.`);
return;
}

const storedId = getIdFromCookie() || getIdFromLocalStorage();
if (storedId) {
return {id: storedId};
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have a question about what I'm doing here. I've noticed the documentation here that states that if we return an id key, then the value will be stored in storage. It also makes it seem that both id and callback are optional. How can I prevent Prebid core from storing the value in storage, as we're already doing this ourselves, yet continue to allow the core logic to access the data?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

After stepping away from this, I think I can answer this question myself: We need to explicitly state in our documentation that the storage config should be omitted, then Prebid core will not store the user ID value itself?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jlukas79 would you mind confirming this for me?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jlukas79 - I'm just making another change. Spotted that we're looking for the consent data in the wrong place. Should update the PR shortly, please do not merge.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@jlukas79 - Please review and merge whenever you are ready.

}

const uspString = uspDataHandler.getConsentData() || '';
const data = {
v: '1',
'1p': includes([1, '1', true], params['1p']) ? '1' : '0',
gdpr: connectIdSubmodule.isEUConsentRequired(consentData) ? '1' : '0',
gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData) ? consentData.gdpr.consentString : '',
us_privacy: consentData && consentData.uspConsent ? consentData.uspConsent : ''
gdpr_consent: connectIdSubmodule.isEUConsentRequired(consentData) ? consentData.consentString : '',
us_privacy: uspString
};

if (connectIdSubmodule.isUnderGPPJurisdiction(consentData)) {
data.gpp = consentData.gppConsent.gppString;
data.gpp_sid = encodeURIComponent(consentData.gppConsent.applicableSections.join(','));
let topmostLocation = getRefererInfo().topmostLocation;
if (typeof topmostLocation === 'string') {
data.url = topmostLocation.split('?')[0];
}

INPUT_PARAM_KEYS.forEach(key => {
Expand All @@ -84,14 +162,19 @@ export const connectIdSubmodule = {
if (response) {
try {
responseObj = JSON.parse(response);
if (isPlainObject(responseObj) && Object.keys(responseObj).length > 0) {
storeObject(responseObj);
} else {
logError(`${MODULE_NAME} module: UPS response returned an invalid payload ${response}`);
}
} catch (error) {
logError(error);
}
}
callback(responseObj);
},
error: error => {
logError(`${MODULE_NAME}: ID fetch encountered an error`, error);
logError(`${MODULE_NAME} module: ID fetch encountered an error`, error);
callback();
}
};
Expand All @@ -108,16 +191,7 @@ export const connectIdSubmodule = {
* @returns {Boolean}
*/
isEUConsentRequired(consentData) {
return !!(consentData && consentData.gdpr && consentData.gdpr.gdprApplies);
},

/**
* Utility function that returns a boolean flag indicating if the opportunity
* is subject to GPP jurisdiction.
* @returns {Boolean}
*/
isUnderGPPJurisdiction(consentData) {
return !!(consentData && consentData.gppConsent && consentData.gppConsent.gppString);
return !!(consentData?.gdprApplies);
},

/**
Expand Down
11 changes: 6 additions & 5 deletions modules/connectIdSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ The below parameters apply only to the Yahoo ConnectID user ID Module.

| Param under usersync.userIds[] | Scope | Type | Description | Example |
| --- | --- | --- | --- | --- |
| name | Required | String | ID value for the Yahoo ConnectID module - `"connectId"` | `"connectId"` |
| params | Required | Object | Data for Yahoo ConnectID initialization. | |
| params.pixelId | Required | Number | The Yahoo supplied publisher specific pixel Id. | `8976` |
| params.he | Optional | String | The SHA-256 hashed user email address. One of either the `he` parameter or the `puid` parameter must be supplied. | `"529cb86de31e9547a712d9f380146e98bbd39beec"` |
| params.puid | Optional | String | The publisher-supplied user identifier. One of either the `he` parameter or the `puid` parameter must be supplied. | `"P-975484817"` |
| name | Required | String | The name of this module. | `"connectId"` |
| params | Required | Object | Container of all module params. ||
| params.pixelId | Required | Number |
The Yahoo-supplied publisher-specific pixel ID. | `"0000"` |
| params.he | Optional | String | The SHA-256 hashed user email address which has been lowercased prior to hashing. Pass both `he` and `puid` params if present, otherwise pass either of the two that is available. |`"ed8ddbf5a171981db8ef938596ca297d5e3f84bcc280041c5880dba3baf9c1d4"`|
| params.puid | Optional | String | The publisher supplied user identifier such as a first-party cookie. Pass both `he` and `puid` params if present, otherwise pass either of the two that is available. | `"ab9iibf5a231ii1db8ef911596ca297d5e3f84biii00041c5880dba3baf9c1da"` |
Loading