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 ID Module : ID5 will be able to optionally delegate its logic to an external module #10742

Merged
merged 14 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from 11 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
242 changes: 176 additions & 66 deletions modules/id5IdSystem.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import {
logWarn,
safeJSONParse
} from '../src/utils.js';
import {ajax} from '../src/ajax.js';
import {fetch} from '../src/ajax.js';
import {submodule} from '../src/hook.js';
import {getRefererInfo} from '../src/refererDetection.js';
import {getStorageManager} from '../src/storageManager.js';
import {uspDataHandler} from '../src/adapterManager.js';
import {MODULE_TYPE_UID} from '../src/activities/modules.js';
import { GreedyPromise } from '../src/utils/promise.js';
import { loadExternalScript } from '../src/adloader.js';

const MODULE_NAME = 'id5Id';
const GVLID = 131;
Expand All @@ -37,6 +39,70 @@ const LEGACY_COOKIE_NAMES = ['pbjs-id5id', 'id5id.1st', 'id5id'];

export const storage = getStorageManager({moduleType: MODULE_TYPE_UID, moduleName: MODULE_NAME});

/**
* @typedef {Object} IdResponse
* @property {string} [universal_uid] - The encrypted ID5 ID to pass to bidders
* @property {Object} [ext] - The extensions object to pass to bidders
* @property {Object} [ab_testing] - A/B testing configuration
*/

/**
* @typedef {Object} FetchCallConfig
* @property {string} [url] - The URL for the fetch endpoint
* @property {Object} [overrides] - Overrides to apply to fetch parameters
*/

/**
* @typedef {Object} ExtensionsCallConfig
* @property {string} [url] - The URL for the extensions endpoint
* @property {string} [method] - Overrides the HTTP method to use to make the call
* @property {Object} [body] - Specifies a body to pass to the extensions endpoint
*/

/**
* @typedef {Object} DynamicConfig
* @property {FetchCallConfig} [fetchCall] - The fetch call configuration
* @property {ExtensionsCallConfig} [extensionsCall] - The extensions call configuration
*/

/**
* @typedef {Object} ABTestingConfig
* @property {boolean} enabled - Tells whether A/B testing is enabled for this instance
* @property {number} controlGroupPct - A/B testing probability
*/

/**
* @typedef {Object} Multiplexing
* @property {boolean} [disabled] - Disable multiplexing (instance will work in single mode)
*/

/**
* @typedef {Object} Diagnostics
* @property {boolean} [publishingDisabled] - Disable diagnostics publishing
* @property {number} [publishAfterLoadInMsec] - Delay in ms after script load after which collected diagnostics are published
* @property {boolean} [publishBeforeWindowUnload] - When true, diagnostics publishing is triggered on Window 'beforeunload' event
* @property {number} [publishingSampleRatio] - Diagnostics publishing sample ratio
*/

/**
* @typedef {Object} Segment
* @property {string} [destination] - GVL ID or ID5-XX Partner ID. Mandatory
* @property {Array<string>} [ids] - The segment IDs to push. Must contain at least one segment ID.
*/

/**
* @typedef {Object} Id5PrebidConfig
* @property {number} partner - The ID5 partner ID
* @property {string} pd - The ID5 partner data string
* @property {ABTestingConfig} abTesting - The A/B testing configuration
* @property {boolean} disableExtensions - Disabled extensions call
* @property {string} [externalModuleUrl] - URL for the id5 prebid external module
* @property {Multiplexing} [multiplexing] - Multiplexing options. Only supported when loading the external module.
* @property {Diagnostics} [diagnostics] - Diagnostics options. Supported only in multiplexing
* @property {Array<Segment>} [segments] - A list of segments to push to partners. Supported only in multiplexing.
* @property {boolean} [disableUaHints] - When true, look up of high entropy values through user agent hints is disabled.
*/

/** @type {Submodule} */
export const id5IdSubmodule = {
/**
Expand Down Expand Up @@ -118,7 +184,8 @@ export const id5IdSubmodule = {
}

const resp = function (cbFunction) {
new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData()).execute()
const fetchFlow = new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData());
fetchFlow.execute()
.then(response => {
cbFunction(response)
})
Expand Down Expand Up @@ -169,91 +236,105 @@ export const id5IdSubmodule = {
},
};

class IdFetchFlow {
export class IdFetchFlow {
constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData) {
this.submoduleConfig = submoduleConfig
this.gdprConsentData = gdprConsentData
this.cacheIdObj = cacheIdObj
this.usPrivacyData = usPrivacyData
}

execute() {
return this.#callForConfig(this.submoduleConfig)
.then(fetchFlowConfig => {
return this.#callForExtensions(fetchFlowConfig.extensionsCall)
.then(extensionsData => {
return this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData)
})
})
.then(fetchCallResponse => {
try {
resetNb(this.submoduleConfig.params.partner);
if (fetchCallResponse.privacy) {
storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS);
}
} catch (error) {
logError(LOG_PREFIX + error);
}
return fetchCallResponse;
})
/**
* Calls the ID5 Servers to fetch an ID5 ID
* @returns {Promise<IdResponse>} The result of calling the server side
*/
async execute() {
const configCallPromise = this.#callForConfig();
if (this.#isExternalModule()) {
try {
return await this.#externalModuleFlow(configCallPromise);
} catch (error) {
logError(LOG_PREFIX + 'Error while performing ID5 external module flow. Continuing with regular flow.', error);
return this.#regularFlow(configCallPromise);
}
} else {
return this.#regularFlow(configCallPromise);
}
}

#isExternalModule() {
return typeof this.submoduleConfig.params.externalModuleUrl === 'string';
}

// eslint-disable-next-line no-dupe-class-members
cosenmarco marked this conversation as resolved.
Show resolved Hide resolved
async #externalModuleFlow(configCallPromise) {
await loadExternalModule(this.submoduleConfig.params.externalModuleUrl);
const fetchFlowConfig = await configCallPromise;

return this.#getExternalIntegration().fetchId5Id(fetchFlowConfig, this.submoduleConfig.params, getRefererInfo(), this.gdprConsentData, this.usPrivacyData);
}

#ajaxPromise(url, data, options) {
return new Promise((resolve, reject) => {
ajax(url,
{
success: function (res) {
resolve(res)
},
error: function (err) {
reject(err)
}
}, data, options)
})
// eslint-disable-next-line no-dupe-class-members
#getExternalIntegration() {
return window.id5Prebid && window.id5Prebid.integration;
}

// eslint-disable-next-line no-dupe-class-members
#callForConfig(submoduleConfig) {
let url = submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only
return this.#ajaxPromise(url, JSON.stringify(submoduleConfig), {method: 'POST'})
.then(response => {
let responseObj = JSON.parse(response);
logInfo(LOG_PREFIX + 'config response received from the server', responseObj);
return responseObj;
});
async #regularFlow(configCallPromise) {
const fetchFlowConfig = await configCallPromise;
const extensionsData = await this.#callForExtensions(fetchFlowConfig.extensionsCall);
const fetchCallResponse = await this.#callId5Fetch(fetchFlowConfig.fetchCall, extensionsData);
return this.#processFetchCallResponse(fetchCallResponse);
}

// eslint-disable-next-line no-dupe-class-members
#callForExtensions(extensionsCallConfig) {
async #callForConfig() {
let url = this.submoduleConfig.params.configUrl || ID5_API_CONFIG_URL; // override for debug/test purposes only
const response = await fetch(url, {
method: 'POST',
body: JSON.stringify(this.submoduleConfig)
});
if (!response.ok) {
throw new Error('Error while calling config endpoint: ', response);
}
const dynamicConfig = await response.json();
logInfo(LOG_PREFIX + 'config response received from the server', dynamicConfig);
return dynamicConfig;
}

// eslint-disable-next-line no-dupe-class-members
async #callForExtensions(extensionsCallConfig) {
if (extensionsCallConfig === undefined) {
return Promise.resolve(undefined)
return undefined;
}
const extensionsUrl = extensionsCallConfig.url;
const method = extensionsCallConfig.method || 'GET';
const body = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {});
const response = await fetch(extensionsUrl, { method, body });
if (!response.ok) {
throw new Error('Error while calling extensions endpoint: ', response);
}
let extensionsUrl = extensionsCallConfig.url
let method = extensionsCallConfig.method || 'GET'
let data = method === 'GET' ? undefined : JSON.stringify(extensionsCallConfig.body || {})
return this.#ajaxPromise(extensionsUrl, data, {'method': method})
.then(response => {
let responseObj = JSON.parse(response);
logInfo(LOG_PREFIX + 'extensions response received from the server', responseObj);
return responseObj;
})
const extensions = await response.json();
logInfo(LOG_PREFIX + 'extensions response received from the server', extensions);
return extensions;
}

// eslint-disable-next-line no-dupe-class-members
#callId5Fetch(fetchCallConfig, extensionsData) {
let url = fetchCallConfig.url;
let additionalData = fetchCallConfig.overrides || {};
let data = {
async #callId5Fetch(fetchCallConfig, extensionsData) {
const fetchUrl = fetchCallConfig.url;
const additionalData = fetchCallConfig.overrides || {};
const body = JSON.stringify({
...this.#createFetchRequestData(),
...additionalData,
extensions: extensionsData
};
return this.#ajaxPromise(url, JSON.stringify(data), {method: 'POST', withCredentials: true})
.then(response => {
let responseObj = JSON.parse(response);
logInfo(LOG_PREFIX + 'fetch response received from the server', responseObj);
return responseObj;
});
});
const response = await fetch(fetchUrl, { method: 'POST', body, credentials: 'include' });
if (!response.ok) {
throw new Error('Error while calling fetch endpoint: ', response);
}
const fetchResponse = await response.json();
logInfo(LOG_PREFIX + 'fetch response received from the server', fetchResponse);
return fetchResponse;
}

// eslint-disable-next-line no-dupe-class-members
Expand All @@ -262,7 +343,7 @@ class IdFetchFlow {
const hasGdpr = (this.gdprConsentData && typeof this.gdprConsentData.gdprApplies === 'boolean' && this.gdprConsentData.gdprApplies) ? 1 : 0;
const referer = getRefererInfo();
const signature = (this.cacheIdObj && this.cacheIdObj.signature) ? this.cacheIdObj.signature : getLegacyCookieSignature();
const nbPage = incrementNb(params.partner);
const nbPage = incrementAndResetNb(params.partner);
const data = {
'partner': params.partner,
'gdpr': hasGdpr,
Expand Down Expand Up @@ -303,6 +384,33 @@ class IdFetchFlow {
}
return data;
}

// eslint-disable-next-line no-dupe-class-members
#processFetchCallResponse(fetchCallResponse) {
try {
if (fetchCallResponse.privacy) {
storeInLocalStorage(ID5_PRIVACY_STORAGE_NAME, JSON.stringify(fetchCallResponse.privacy), NB_EXP_DAYS);
}
} catch (error) {
logError(LOG_PREFIX + 'Error while writing privacy info into local storage.', error);
}
return fetchCallResponse;
}
}

async function loadExternalModule(url) {
return new GreedyPromise((resolve, reject) => {
if (window.id5Prebid) {
// Already loaded
resolve();
} else {
try {
loadExternalScript(url, 'id5', resolve);
} catch (error) {
reject(error);
}
}
});
}

function validateConfig(config) {
Expand Down Expand Up @@ -365,8 +473,10 @@ function incrementNb(partnerId) {
return nb;
}

function resetNb(partnerId) {
function incrementAndResetNb(partnerId) {
const result = incrementNb(partnerId);
storeNbInCache(partnerId, 0);
return result;
}

function getLegacyCookieSignature() {
Expand Down
2 changes: 2 additions & 0 deletions modules/id5IdSystem.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ pbjs.setConfig({
name: 'id5Id',
params: {
partner: 173, // change to the Partner Number you received from ID5
externalModuleUrl: "https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js" // optional but recommended
pd: 'MT1iNTBjY...', // optional, see table below for a link to how to generate this
abTesting: { // optional
enabled: true, // false by default
Expand All @@ -49,6 +50,7 @@ pbjs.setConfig({
| name | Required | String | The name of this module: `"id5Id"` | `"id5Id"` |
| params | Required | Object | Details for the ID5 ID. | |
| params.partner | Required | Number | This is the ID5 Partner Number obtained from registering with ID5. | `173` |
| params.externalModuleUrl | Optional | String | The URL for the id5-prebid external module. It is recommended to use the latest version at the URL in the example. Source code available [here](https://github.com/id5io/id5-api.js/blob/master/src/id5PrebidModule.js). | https://cdn.id5-sync.com/api/1.0/id5PrebidModule.js
| params.pd | Optional | String | Partner-supplied data used for linking ID5 IDs across domains. See [our documentation](https://wiki.id5.io/en/identitycloud/retrieve-id5-ids/passing-partner-data-to-id5) for details on generating the string. Omit the parameter or leave as an empty string if no data to supply | `"MT1iNTBjY..."` |
| params.provider | Optional | String | An identifier provided by ID5 to technology partners who manage Prebid setups on behalf of publishers. Reach out to [ID5](mailto:prebid@id5.io) if you have questions about this parameter | `pubmatic-identity-hub` |
| params.abTesting | Optional | Object | Allows publishers to easily run an A/B Test. If enabled and the user is in the Control Group, the ID5 ID will NOT be exposed to bid adapters for that request | Disabled by default |
Expand Down
3 changes: 2 additions & 1 deletion src/adloader.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ const _approvedLoadExternalJSList = [
'geoedge',
'mediafilter',
'qortex',
'dynamicAdBoost'
'dynamicAdBoost',
'id5'
]

/**
Expand Down
Loading