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

ID5 user id module: migrate publishers to use local storage instead of 1p cookies #5874

Merged
Merged
Show file tree
Hide file tree
Changes from 9 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
147 changes: 113 additions & 34 deletions modules/id5IdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,13 @@ import { getStorageManager } from '../src/storageManager.js';

const MODULE_NAME = 'id5Id';
const GVLID = 131;
const BASE_NB_COOKIE_NAME = 'id5id.1st';
const NB_COOKIE_EXP_DAYS = (30 * 24 * 60 * 60 * 1000); // 30 days
const NB_EXP_DAYS = (30 * 24 * 60 * 60 * 1000); // 30 days
export const ID5_STORAGE_NAME = 'id5id';
const LOCAL_STORAGE = 'html5';

// order the legacy cookie names in reverse priority order so the last
// cookie in the array is the most preferred to use
const LEGACY_COOKIE_NAMES = [ 'pbjs-id5id', 'id5id.1st' ];

const storage = getStorageManager(GVLID, MODULE_NAME);

Expand Down Expand Up @@ -42,10 +47,7 @@ export const id5IdSubmodule = {
let uid;
let linkType = 0;

if (value && typeof value.ID5ID === 'string') {
// don't lose our legacy value from cache
uid = value.ID5ID;
} else if (value && typeof value.universal_uid === 'string') {
if (value && typeof value.universal_uid === 'string') {
uid = value.universal_uid;
linkType = value.link_type || linkType;
} else {
Expand All @@ -71,22 +73,20 @@ export const id5IdSubmodule = {
* @returns {IdResponse|undefined}
*/
getId(config, consentData, cacheIdObj) {
const configParams = (config && config.params) || {};
if (!hasRequiredParams(configParams)) {
if (!hasRequiredConfig(config)) {
return undefined;
}

const hasGdpr = (consentData && typeof consentData.gdprApplies === 'boolean' && consentData.gdprApplies) ? 1 : 0;
const gdprConsentString = hasGdpr ? consentData.consentString : '';
const url = `https://id5-sync.com/g/v2/${configParams.partner}.json?gdpr_consent=${gdprConsentString}&gdpr=${hasGdpr}`;
const url = `https://id5-sync.com/g/v2/${config.params.partner}.json?gdpr_consent=${gdprConsentString}&gdpr=${hasGdpr}`;
const referer = getRefererInfo();
const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : '';
const pubId = (cacheIdObj && cacheIdObj.ID5ID) ? cacheIdObj.ID5ID : ''; // TODO: remove when 1puid isn't needed
const signature = (cacheIdObj && cacheIdObj.signature) ? cacheIdObj.signature : getLegacyCookieSignature();
const data = {
'partner': configParams.partner,
'1puid': pubId, // TODO: remove when 1puid isn't needed
'nbPage': incrementNb(configParams),
'partner': config.params.partner,
'nbPage': incrementNb(config.params.partner),
'o': 'pbjs',
'pd': configParams.pd || '',
'pd': config.params.pd || '',
'rf': referer.referer,
's': signature,
'top': referer.reachedTop ? 1 : 0,
Expand All @@ -101,15 +101,21 @@ export const id5IdSubmodule = {
if (response) {
try {
responseObj = JSON.parse(response);
resetNb(configParams);
resetNb(config.params.partner);

// TODO: remove after requiring publishers to use localstorage and
// all publishers have upgraded
if (config.storage.type === LOCAL_STORAGE) {
removeLegacyCookies(config.params.partner);
}
} catch (error) {
utils.logError(error);
}
}
callback(responseObj);
},
error: error => {
utils.logError(`id5Id: ID fetch encountered an error`, error);
utils.logError(`User ID - ID5 submodule getId fetch encountered an error`, error);
callback();
}
};
Expand All @@ -129,39 +135,112 @@ export const id5IdSubmodule = {
* @return {(IdResponse|function(callback:function))} A response object that contains id and/or callback.
*/
extendId(config, cacheIdObj) {
const configParams = (config && config.params) || {};
incrementNb(configParams);
const partnerId = (config && config.params && config.params.partner) || 0;
incrementNb(partnerId);
return cacheIdObj;
}
};

function hasRequiredParams(configParams) {
if (!configParams || typeof configParams.partner !== 'number') {
function hasRequiredConfig(config) {
if (!config || !config.params || !config.params.partner || typeof config.params.partner !== 'number') {
utils.logError(`User ID - ID5 submodule requires partner to be defined as a number`);
return false;
}

if (!config || !config.storage || !config.storage.type || !config.storage.name) {
smenzer marked this conversation as resolved.
Show resolved Hide resolved
utils.logError(`User ID - ID5 submodule requires storage to be set`);
return false;
}

// TODO: in a future release, return false if storage type or name are not set as required
if (config.storage.type !== LOCAL_STORAGE) {
utils.logWarn(`User ID - ID5 submodule recommends storage type to be '${LOCAL_STORAGE}'. In a future release this will become a strict requirement`);
}
// TODO: in a future release, return false if storage type or name are not set as required
if (config.storage.name !== ID5_STORAGE_NAME) {
utils.logWarn(`User ID - ID5 submodule recommends storage name to be '${ID5_STORAGE_NAME}'. In a future release this will become a strict requirement`);
}

return true;
}
function nbCookieName(configParams) {
return hasRequiredParams(configParams) ? `${BASE_NB_COOKIE_NAME}_${configParams.partner}_nb` : undefined;

export function expDaysStr(expDays) {
return (new Date(Date.now() + (60 * 60 * 24 * expDays))).toUTCString();
smenzer marked this conversation as resolved.
Show resolved Hide resolved
}
function nbCookieExpStr(expDays) {
return (new Date(Date.now() + expDays)).toUTCString();

export function nbCacheName(partnerId) {
return `${ID5_STORAGE_NAME}_${partnerId}_nb`;
}
function storeNbInCookie(configParams, nb) {
storage.setCookie(nbCookieName(configParams), nb, nbCookieExpStr(NB_COOKIE_EXP_DAYS), 'Lax');
export function storeNbInCache(partnerId, nb) {
storeInLocalStorage(nbCacheName(partnerId), nb, NB_EXP_DAYS);
}
function getNbFromCookie(configParams) {
const cacheNb = storage.getCookie(nbCookieName(configParams));
export function getNbFromCache(partnerId) {
let cacheNb = getFromLocalStorage(nbCacheName(partnerId));
return (cacheNb) ? parseInt(cacheNb) : 0;
}
function incrementNb(configParams) {
const nb = (getNbFromCookie(configParams) + 1);
storeNbInCookie(configParams, nb);
function incrementNb(partnerId) {
const nb = (getNbFromCache(partnerId) + 1);
storeNbInCache(partnerId, nb);
return nb;
}
function resetNb(configParams) {
storeNbInCookie(configParams, 0);
function resetNb(partnerId) {
storeNbInCache(partnerId, 0);
}

function getLegacyCookieSignature() {
let legacyStoredValue;
LEGACY_COOKIE_NAMES.forEach(function(cookie) {
if (storage.getCookie(cookie)) {
legacyStoredValue = JSON.parse(storage.getCookie(cookie)) || legacyStoredValue;
}
});
return (legacyStoredValue && legacyStoredValue.signature) || '';
}

/**
* Remove our legacy cookie values. Needed until we move all publishers
* to html5 storage in a future release
* @param {integer} partnerId
*/
function removeLegacyCookies(partnerId) {
LEGACY_COOKIE_NAMES.forEach(function(cookie) {
storage.setCookie(`${cookie}`, '', expDaysStr(-1));
storage.setCookie(`${cookie}_nb`, '', expDaysStr(-1));
storage.setCookie(`${cookie}_${partnerId}_nb`, '', expDaysStr(-1));
storage.setCookie(`${cookie}_last`, '', expDaysStr(-1));
});
}

/**
* This will make sure we check for expiration before accessing local storage
* @param {string} key
*/
export function getFromLocalStorage(key) {
const storedValueExp = storage.getDataFromLocalStorage(`${key}_exp`);
// empty string means no expiration set
if (storedValueExp === '') {
return storage.getDataFromLocalStorage(key);
} else if (storedValueExp) {
if ((new Date(storedValueExp)).getTime() - Date.now() > 0) {
return storage.getDataFromLocalStorage(key);
}
}
// if we got here, then we have an expired item or we didn't set an
// expiration initially somehow, so we need to remove the item from the
// local storage
storage.removeDataFromLocalStorage(key);
return null;
}
/**
* Ensure that we always set an expiration in local storage since
* by default it's not required
* @param {string} key
* @param {any} value
* @param {integer} expDays
*/
export function storeInLocalStorage(key, value, expDays) {
storage.setDataInLocalStorage(`${key}_exp`, expDaysStr(expDays));
storage.setDataInLocalStorage(`${key}`, value);
}

submodule('userId', id5IdSubmodule);
54 changes: 54 additions & 0 deletions modules/id5IdSystem.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# ID5 Universal ID

The ID5 Universal ID is a shared, neutral identifier that publishers and ad tech platforms can use to recognise users even in environments where 3rd party cookies are not available. The ID5 Universal ID is designed to respect users' privacy choices and publishers’ preferences throughout the advertising value chain. For more information about the ID5 Universal ID and detailed integration docs, please visit [our documentation](https://console.id5.io/docs/public/prebid). We also recommend that you sign up for our [release notes](https://id5.io/universal-id/release-notes) to stay up-to-date with any changes to the implementation of the ID5 Universal ID in Prebid.

## ID5 Universal ID Registration

The ID5 Universal ID is free to use, but requires a simple registration with ID5. Please visit [id5.io/universal-id](https://id5.io/universal-id) to sign up and request your ID5 Partner Number to get started.

The ID5 privacy policy is at [https://www.id5.io/platform-privacy-policy](https://www.id5.io/platform-privacy-policy).

## ID5 Universal ID Configuration

First, make sure to add the ID5 submodule to your Prebid.js package with:

```
gulp build --modules=id5IdSystem,userId
```

The following configuration parameters are available:

```javascript
pbjs.setConfig({
userSync: {
userIds: [{
name: "id5Id",
params: {
partner: 173, // change to the Partner Number you received from ID5
pd: "MT1iNTBjY..." // optional, see table below for a link to how to generate this
},
storage: {
type: "html5", // "html5" is the required storage type
name: "id5id", // "id5id" is the required storage name
expires: 90, // storage lasts for 90 days
refreshInSeconds: 8*3600 // refresh ID every 8 hours to ensure it's fresh
}
}],
auctionDelay: 50 // 50ms maximum auction delay, applies to all userId modules
}
});
```

| Param under userSync.userIds[] | Scope | Type | Description | Example |
| --- | --- | --- | --- | --- |
| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` |
| params | Required | Object | Details for the ID5 Universal ID. | |
| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` |
| params.pd | Optional | String | Publisher-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/x/BIAZ) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` |
| storage | Required | Object | Storage settings for how the User ID module will cache the ID5 ID locally | |
| storage.type | Required | String | This is where the results of the user ID will be stored. ID5 **requires** `"html5"`. | `"html5"` |
| storage.name | Required | String | The name of the local storage where the user ID will be stored. ID5 **requires** `"id5id"`. | `"id5id"` |
| storage.expires | Optional | Integer | How long (in days) the user ID information will be stored. ID5 recommends `90`. | `90` |
| storage.refreshInSeconds | Optional | Integer | How many seconds until the ID5 ID will be refreshed. ID5 strongly recommends 8 hours between refreshes | `8*3600` |

**ATTENTION:** As of Prebid.js v4.13.0, ID5 requires `storage.type` to be `"html5"` and `storage.name` to be `"id5id"`. Using other values will display a warning today, but in an upcoming release, it will prevent the ID5 module from loading. This change is to ensure the ID5 module in Prebid.js interoperates properly with the [ID5 API](https://github.com/id5io/id5-api.js) and to reduce the size of publishers' first-party cookies that are sent to their web servers. If you have any questions, please reach out to us at [[email protected]](mailto:[email protected]).
10 changes: 5 additions & 5 deletions modules/userId/userId.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ pbjs.setConfig({
pd: "some-pd-string" // See https://wiki.id5.io/x/BIAZ for details
},
storage: {
type: "cookie",
name: "id5id.1st",
expires: 90, // Expiration of cookies in days
type: "html5", // ID5 requires html5
name: "id5id",
expires: 90, // Expiration in days
refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires'
},
}, {
Expand Down Expand Up @@ -144,8 +144,8 @@ pbjs.setConfig({
},
storage: {
type: 'html5',
name: 'id5id.1st',
expires: 90, // Expiration of cookies in days
name: 'id5id',
expires: 90, // Expiration in days
refreshInSeconds: 8*3600 // User Id cache lifetime in seconds, defaulting to 'expires'
},
}, {
Expand Down
Loading