Skip to content

Commit

Permalink
Weborama RTD Module : update gdpr purpose ids verification for TCF v2…
Browse files Browse the repository at this point in the history
….2 (prebid#11089)

* add gdpr verification for user-centric modules

* fix log messages

* refactor logger

* small refactors in code and import order

* add new purpose ids and special feature 1

* remove special feature one, keep purposes 1, 3, 4, 5 and 7

* remove special feature one, keep purposes 1, 3, 4, 5 and 7

* remove special feature one, keep purposes 1, 3, 4, 5 and 6

* fix lint warning jsdoc/no-undefined-types

* fix typos

* update doc

* fix link in doc
  • Loading branch information
peczenyj committed Mar 14, 2024
1 parent e939aee commit e45e5ca
Show file tree
Hide file tree
Showing 3 changed files with 226 additions and 40 deletions.
135 changes: 99 additions & 36 deletions modules/weboramaRtdProvider.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,18 +104,18 @@ import {
getGlobal
} from '../src/prebidGlobal.js';
import {
deepAccess,
deepClone,
deepSetValue,
isEmpty,
isFn,
logError,
logMessage,
isArray,
isStr,
isBoolean,
isEmpty,
isFn,
isPlainObject,
isStr,
logWarn,
mergeDeep
mergeDeep,
prefixLog,
} from '../src/utils.js';
import {
submodule
Expand All @@ -126,9 +126,13 @@ import {
import {
getStorageManager
} from '../src/storageManager.js';
import {
MODULE_TYPE_RTD
} from '../src/activities/modules.js';
import adapterManager from '../src/adapterManager.js';
import {MODULE_TYPE_RTD} from '../src/activities/modules.js';
import {tryAppendQueryString} from '../libraries/urlUtils/urlUtils.js';
import {
tryAppendQueryString
} from '../libraries/urlUtils/urlUtils.js';

/** @type {string} */
const MODULE_NAME = 'realTimeData';
Expand Down Expand Up @@ -159,6 +163,8 @@ const SFBX_LITE_DATA_SOURCE_LABEL = 'lite';
/** @type {number} */
const GVLID = 284;

const logger = prefixLog('[WeboramaRTD]');

export const storage = getStorageManager({
moduleType: MODULE_TYPE_RTD,
moduleName: SUBMODULE_NAME
Expand Down Expand Up @@ -199,9 +205,11 @@ class WeboramaRtdProvider {
* @method
* @param {Object} moduleConfig
* @param {?ModuleParams} moduleConfig.params
* @param {Object} userConsent
* @param {?Object} userConsent.gdpr
* @return {boolean} true if module was initialized with success
*/
init(moduleConfig) {
init(moduleConfig, userConsent) {
/** @type {Object} */
const globalDefaults = {
setPrebidTargeting: true,
Expand All @@ -219,8 +227,14 @@ class WeboramaRtdProvider {
this.#components.WeboUserData.data = null;
this.#components.SfbxLiteData.data = null;

this.#components.WeboCtx.initialized = this.#initSubSection(moduleParams, WEBO_CTX_CONF_SECTION, 'token');
this.#components.WeboUserData.initialized = this.#initSubSection(moduleParams, WEBO_USER_DATA_CONF_SECTION);
const weboCtxRequiredFields = ['token'];

this.#components.WeboCtx.initialized = this.#initSubSection(moduleParams, WEBO_CTX_CONF_SECTION, {
requiredFields: weboCtxRequiredFields,
});
this.#components.WeboUserData.initialized = this.#initSubSection(moduleParams, WEBO_USER_DATA_CONF_SECTION, {
userConsent: userConsent || {},
});
this.#components.SfbxLiteData.initialized = this.#initSubSection(moduleParams, SFBX_LITE_DATA_CONF_SECTION);

return Object.values(this.#components).some((c) => c.initialized);
Expand Down Expand Up @@ -251,7 +265,7 @@ class WeboramaRtdProvider {
const weboCtxConf = moduleParams.weboCtxConf || {};

this.#fetchContextualProfile(weboCtxConf, (data) => {
logMessage('fetchContextualProfile on getBidRequestData is done');
logger.logMessage('fetchContextualProfile on getBidRequestData is done');

this.#setWeboContextualProfile(data);
}, () => {
Expand All @@ -276,17 +290,17 @@ class WeboramaRtdProvider {
const profileHandlers = this.#buildProfileHandlers(moduleParams);

if (isEmpty(profileHandlers)) {
logMessage('no data to set targeting');
logger.logMessage('no data to set targeting');
return {};
}

try {
return adUnitsCodes.reduce((data, adUnitCode) => {
data[adUnitCode] = profileHandlers.reduce((targeting, ph) => {
// logMessage(`check if should set targeting for adunit '${adUnitCode}'`);
// logger.logMessage(`check if should set targeting for adunit '${adUnitCode}'`);
const [data, metadata] = this.#copyDataAndMetadata(ph);
if (ph.setTargeting(adUnitCode, data, metadata)) {
// logMessage(`set targeting for adunit '${adUnitCode}', source '${metadata.source}'`);
// logger.logMessage(`set targeting for adunit '${adUnitCode}', source '${metadata.source}'`);

mergeDeep(targeting, data);
}
Expand All @@ -297,7 +311,7 @@ class WeboramaRtdProvider {
return data;
}, {});
} catch (e) {
logError(`unable to format weborama rtd targeting data:`, e);
logger.logError(`unable to format weborama rtd targeting data:`, e);

return {};
}
Expand All @@ -309,10 +323,12 @@ class WeboramaRtdProvider {
* @private
* @param {ModuleParams} moduleParams
* @param {string} subSection subsection name to initialize
* @param {string[]} requiredFields
* @param {?Object} extra
* @param {string[]} extra.requiredFields
* @param {?Object} extra.userConsent
* @return {boolean} true if module subsection was initialized with success
*/
#initSubSection(moduleParams, subSection, ...requiredFields) {
#initSubSection(moduleParams, subSection, extra) {
/** @type {CommonConf} */
const weboSectionConf = moduleParams[subSection] || { enabled: false };

Expand All @@ -325,21 +341,59 @@ class WeboramaRtdProvider {
try {
this.#normalizeConf(moduleParams, weboSectionConf);

extra = extra || {};
const requiredFields = extra?.requiredFields || [];

requiredFields.forEach(field => {
if (!(field in weboSectionConf)) {
throw `missing required field '${field}''`;
throw `missing required field '${field}'`;
}
});

if (isPlainObject(extra?.userConsent?.gdpr) && !this.#checkTCFv2(extra.userConsent.gdpr)) {
throw 'gdpr consent not ok';
}
} catch (e) {
logError(`unable to initialize: error on ${subSection} configuration:`, e);
logger.logError(`unable to initialize: error on '${subSection}' configuration:`, e);
return false;
}

logMessage(`weborama ${subSection} initialized with success`);
logger.logMessage(`weborama '${subSection}' initialized with success`);

return true;
}

/**
* check gdpr consent data
* @method
* @private
* @param {Object} gdpr
* @param {?boolean} gdpr.gdprApplies
* @param {?Object} gdpr.vendorData
* @param {?Object} gdpr.vendorData.purpose
* @param {?Object.<number, boolean>} gdpr.vendorData.purpose.consents
* @param {?Object} gdpr.vendorData.vendor
* @param {?Object.<number, boolean>} gdpr.vendorData.vendor.consents
* @return {boolean}
*/
// eslint-disable-next-line no-dupe-class-members
#checkTCFv2(gdpr) {
if (gdpr?.gdprApplies !== true) {
return true;
}

if (deepAccess(gdpr, 'vendorData.vendor.consents') &&
deepAccess(gdpr, 'vendorData.purpose.consents')) {
return gdpr.vendorData.vendor.consents[GVLID] === true && // check weborama vendor id
gdpr.vendorData.purpose.consents[1] === true && // info storage access
gdpr.vendorData.purpose.consents[3] === true && // create personalized ads
gdpr.vendorData.purpose.consents[4] === true && // select personalized ads
gdpr.vendorData.purpose.consents[5] === true && // create personalized content
gdpr.vendorData.purpose.consents[6] === true; // select personalized content
}

return true;
}
/**
* normalize submodule configuration
* @method
Expand Down Expand Up @@ -455,7 +509,7 @@ class WeboramaRtdProvider {
const profileHandlers = this.#buildProfileHandlers(moduleParams);

if (isEmpty(profileHandlers)) {
logMessage('no data to send to bidders');
logger.logMessage('no data to send to bidders');
return;
}

Expand All @@ -465,27 +519,27 @@ class WeboramaRtdProvider {
adUnits.forEach(
adUnit => adUnit.bids?.forEach(
bid => profileHandlers.forEach(ph => {
// logMessage(`check if bidder '${bid.bidder}' and adunit '${adUnit.code} are share ${ph.metadata.source} data`);
// logger.logMessage(`check if bidder '${bid.bidder}' and adunit '${adUnit.code} are share ${ph.metadata.source} data`);

const [data, metadata] = this.#copyDataAndMetadata(ph);
if (ph.sendToBidders(bid, adUnit.code, data, metadata)) {
// logMessage(`handling bidder '${bid.bidder}' with ${ph.metadata.source} data`);
// logger.logMessage(`handling bidder '${bid.bidder}' with ${ph.metadata.source} data`);

this.#handleBid(reqBidsConfigObj, bid, data, ph.metadata);
}
})
)
);
} catch (e) {
logError('unable to send data to bidders:', e);
logger.logError('unable to send data to bidders:', e);
}

profileHandlers.forEach(ph => {
try {
const [data, metadata] = this.#copyDataAndMetadata(ph);
ph.onData(data, metadata);
} catch (e) {
logError(`error while execute onData callback with ${ph.metadata.source}-based data:`, e);
logger.logError(`error while execute onData callback with ${ph.metadata.source}-based data:`, e);
}
});
}
Expand Down Expand Up @@ -529,7 +583,7 @@ class WeboramaRtdProvider {
try {
assetID = weboCtxConf.assetID();
} catch (e) {
logError('unexpected error while fetching asset id from callback', e);
logger.logError('unexpected error while fetching asset id from callback', e);

onDone();

Expand All @@ -538,7 +592,7 @@ class WeboramaRtdProvider {
}

if (!assetID) {
logError('missing asset id');
logger.logError('missing asset id');

onDone();

Expand All @@ -565,7 +619,7 @@ class WeboramaRtdProvider {
};

const error = (e, req) => {
logError(`unable to get weborama data`, e, req);
logger.logError(`unable to get weborama data`, e, req);

onDone();
};
Expand Down Expand Up @@ -625,7 +679,7 @@ class WeboramaRtdProvider {
if (profileHandler) {
ph.push(profileHandler);
} else {
logMessage(`skip ${source} profile: no data`);
logger.logMessage(`skip ${source} profile: no data`);
}

return ph;
Expand Down Expand Up @@ -703,17 +757,26 @@ class WeboramaRtdProvider {
#handleBid(reqBidsConfigObj, bid, profile, metadata) {
this.#handleBidViaORTB2(reqBidsConfigObj, bid.bidder, profile, metadata);

/** @type {Object.<string,string>} */
const bidderAliasRegistry = adapterManager.aliasRegistry || {};

/** @type {string} */
const bidder = bidderAliasRegistry[bid.bidder] || bid.bidder;
const bidder = this.#getAdapterNameForAlias(bid.bidder);

if (bidder == 'appnexus') {
this.#handleAppnexusBid(reqBidsConfigObj, bid, profile);
}
}

/**
* return adapter name based on alias, if any
* @method
* @private
* @param {string} aliasName
* @returns {string}
*/
// eslint-disable-next-line no-dupe-class-members
#getAdapterNameForAlias(aliasName) {
return adapterManager.aliasRegistry[aliasName] || aliasName;
}

/**
* function that handles bid request data
* @method
Expand Down Expand Up @@ -760,13 +823,13 @@ class WeboramaRtdProvider {
// eslint-disable-next-line no-dupe-class-members
#handleBidViaORTB2(reqBidsConfigObj, bidder, profile, metadata) {
if (isBoolean(metadata.user)) {
logMessage(`bidder '${bidder}' is not directly supported, trying set data via bidder ortb2 fpd`);
logger.logMessage(`bidder '${bidder}' is not directly supported, trying set data via bidder ortb2 fpd`);
const section = metadata.user ? 'user' : 'site';
const path = `${section}.ext.data`;

this.#setBidderOrtb2(reqBidsConfigObj.ortb2Fragments?.bidder, bidder, path, profile)
} else {
logMessage(`SKIP unsupported bidder '${bidder}', data from '${metadata.source}' is not defined as user or site-centric`);
logger.logMessage(`SKIP unsupported bidder '${bidder}', data from '${metadata.source}' is not defined as user or site-centric`);
}
}
/**
Expand Down
17 changes: 13 additions & 4 deletions modules/weboramaRtdProvider.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Weborama provides a Real-Time Data Submodule for `Prebid.js`, allowing to easy i

* LiTE by SFBX® (Local inApp Trust Engine) provides “Zero Party Data” given by users, stored and calculated only on the user’s device. Through a unique cohorting system, it enables better monetization in a consent/consentless and identity-less mode.

Contact [email protected] for more information.
Contact [[email protected]] for more information.

### Publisher Usage

Expand Down Expand Up @@ -79,7 +79,7 @@ pbjs.setConfig({

Each module can perform two actions:

* set targeting on [GPT](https://docs.prebid.org/dev-docs/publisher-api-reference/setTargetingForGPTAsync.html) / [AST](https://docs.prebid.org/dev-docs/publisher-api-reference/setTargetingForAst.html]) via `prebid.js`
* set targeting on [GPT](https://docs.prebid.org/dev-docs/publisher-api-reference/setTargetingForGPTAsync.html) / [AST](https://docs.prebid.org/dev-docs/publisher-api-reference/setTargetingForAst.html) via `prebid.js`

* send data to other `prebid.js` bidder modules (check the complete list at the end of this page)

Expand Down Expand Up @@ -117,9 +117,9 @@ On this section we will explain the `params.weboCtxConf` subconfiguration:
| enabled | Boolean| if false, will ignore this configuration| Default is `true` if this section is present|
| baseURLProfileAPI | String| if present, update the domain of the contextual api| Optional. Default is `ctx.weborama.com` |

#### WAM User-Centric Configuration
#### User-Centric Configuration

To be possible use the integration with Weborama Audience Manager (WAM) you must be a client with an account id and you lust include the `wamfactory` script in your pages with `wam2gam` feature activated.
To be possible use the integration with Weborama Audience Manager (WAM) you must be a client with an account id and you must include the `wamfactory` script in your pages with `wam2gam` feature activated.
Please contact weborama if you don't have it.

On this section we will explain the `params.weboUserDataConf` subconfiguration:
Expand All @@ -134,6 +134,15 @@ On this section we will explain the `params.weboUserDataConf` subconfiguration:
| localStorageProfileKey| String | can be used to customize the local storage key | Optional |
| enabled | Boolean| if false, will ignore this configuration| Default is `true` if this section is present|

##### User Consent

The WAM User-Centric configuration will check for user consent if gdpr applies. It will check for consent:

* Vendor ID 284 (Weborama)
* Purpose IDs: 1, 3, 4, 5 and 6

If the user consent does not match such conditions, this module will not load, means we will not check for any data in local storage and the default profile will be ignored.

#### Sfbx LiTE Site-Centric Configuration

To be possible use the integration between Weborama and Sfbx LiTE you should also contact SFBX® to setup this product.
Expand Down
Loading

0 comments on commit e45e5ca

Please sign in to comment.