Skip to content

Commit

Permalink
Yahoo ConnectId UserID Module: explicit storage management (#9716)
Browse files Browse the repository at this point in the history
* Explicitly manage storage of ConnectID

* Addressed initial PR feedback

* Documentation update

* Address consent logic issues

* Prevent storage of empty object response from UPS

* Change storage key to match module name.

---------

Co-authored-by: slimkrazy <sam@slimkrazy.com>
  • Loading branch information
slimkrazy and slimkrazy authored Apr 12, 2023
1 parent dbc6485 commit f26f7db
Show file tree
Hide file tree
Showing 3 changed files with 521 additions and 268 deletions.
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};
}

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

0 comments on commit f26f7db

Please sign in to comment.