Skip to content

Commit

Permalink
ID5 ID Module : ID5 will be able to optionally delegate its logic to …
Browse files Browse the repository at this point in the history
…an external module (#10742)

* id-7317 Adding ability to load exernal module by param configuration

* id-7317 Fixing bugs with id5 external module

* id-7313 Addinf documentation to new externalModuleUrl parameter

* id-7317 Typo

* id-7317 Fix Lint error

* id-7317 Some improvements from PR

* id-7317 Some test iprovements

* id-7317 Using loadExternalScript() utility instead of loading the script directly

* id-7317 Lint error

* id-7317 Fixing nb increments

* ID5 User Id module - pass gpp consent data to external module

---------

Co-authored-by: abazylewicz <abazylewicz@id5.io>
Co-authored-by: abazylewicz-id5 <106807984+abazylewicz-id5@users.noreply.github.com>
  • Loading branch information
3 people authored Jan 23, 2024
1 parent 5a73e16 commit c6a9ebc
Show file tree
Hide file tree
Showing 4 changed files with 713 additions and 567 deletions.
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, gppDataHandler} 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(), gppDataHandler.getConsentData()).execute()
const fetchFlow = new IdFetchFlow(submoduleConfig, consentData, cacheIdObj, uspDataHandler.getConsentData(), gppDataHandler.getConsentData());
fetchFlow.execute()
.then(response => {
cbFunction(response)
})
Expand Down Expand Up @@ -169,7 +236,7 @@ export const id5IdSubmodule = {
},
};

class IdFetchFlow {
export class IdFetchFlow {
constructor(submoduleConfig, gdprConsentData, cacheIdObj, usPrivacyData, gppData) {
this.submoduleConfig = submoduleConfig
this.gdprConsentData = gdprConsentData
Expand All @@ -178,83 +245,97 @@ class IdFetchFlow {
this.gppData = gppData
}

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
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, this.gppData);
}

#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 @@ -263,7 +344,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 @@ -309,6 +390,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 @@ -371,8 +479,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 @@ -30,7 +30,8 @@ const _approvedLoadExternalJSList = [
'mediafilter',
'qortex',
'dynamicAdBoost',
'contxtful'
'contxtful',
'id5'
]

/**
Expand Down
Loading

0 comments on commit c6a9ebc

Please sign in to comment.