Skip to content

Commit

Permalink
Multiple modules: USP data deletion event handling
Browse files Browse the repository at this point in the history
  • Loading branch information
dgirardi committed Oct 14, 2022
1 parent 5e18b2d commit 90db169
Show file tree
Hide file tree
Showing 6 changed files with 362 additions and 37 deletions.
25 changes: 19 additions & 6 deletions modules/consentManagementUsp.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
*/
import {isFn, isNumber, isPlainObject, isStr, logError, logInfo, logWarn} from '../src/utils.js';
import {config} from '../src/config.js';
import {uspDataHandler} from '../src/adapterManager.js';
import adapterManager, {uspDataHandler} from '../src/adapterManager.js';
import {timedAuctionHook} from '../src/utils/perfMetrics.js';
import {getHook} from '../src/hook.js';

Expand Down Expand Up @@ -114,6 +114,11 @@ function lookupUspConsent({onSuccess, onError}) {
USPAPI_VERSION,
callbackHandler.consentDataCallback
);
uspapiFunction(
'registerDeletion',
USPAPI_VERSION,
adapterManager.callDataDeletionRequest
)
} else {
logInfo(
'Detected USP CMP is outside the current iframe where Prebid.js is located, calling it now...'
Expand All @@ -123,12 +128,17 @@ function lookupUspConsent({onSuccess, onError}) {
uspapiFrame,
callbackHandler.consentDataCallback
);
callUspApiWhileInIframe(
'registerDeletion',
uspapiFrame,
adapterManager.callDataDeletionRequest
);
}

let listening = false;

function callUspApiWhileInIframe(commandName, uspapiFrame, moduleCallback) {
/* Setup up a __uspapi function to do the postMessage and stash the callback.
This function behaves, from the caller's perspective, identicially to the in-frame __uspapi call (although it is not synchronous) */
window.__uspapi = function (cmd, ver, callback) {
function callUsp(cmd, ver, callback) {
let callId = Math.random() + '';
let msg = {
__uspapiCall: {
Expand All @@ -143,10 +153,13 @@ function lookupUspConsent({onSuccess, onError}) {
};

/** when we get the return message, call the stashed callback */
window.addEventListener('message', readPostMessageResponse, false);
if (!listening) {
window.addEventListener('message', readPostMessageResponse, false);
listening = true;
}

// call uspapi
window.__uspapi(commandName, USPAPI_VERSION, moduleCallback);
callUsp(commandName, USPAPI_VERSION, moduleCallback);

function readPostMessageResponse(event) {
const res = event && event.data && event.data.__uspapiReturn;
Expand Down
59 changes: 54 additions & 5 deletions modules/userId/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ import {find, includes} from '../../src/polyfill.js';
import {config} from '../../src/config.js';
import * as events from '../../src/events.js';
import {getGlobal} from '../../src/prebidGlobal.js';
import {gdprDataHandler} from '../../src/adapterManager.js';
import adapterManager, {gdprDataHandler} from '../../src/adapterManager.js';
import CONSTANTS from '../../src/constants.json';
import {hook, module, ready as hooksReady} from '../../src/hook.js';
import {buildEidPermissions, createEidsArray, USER_IDS_CONFIG} from './eids.js';
Expand Down Expand Up @@ -216,6 +216,14 @@ export function setSubmoduleRegistry(submodules) {
submoduleRegistry = submodules;
}

function cookieSetter(submodule) {
const domainOverride = (typeof submodule.submodule.domainOverride === 'function') ? submodule.submodule.domainOverride() : null;
const name = submodule.config.storage.name;
return function setCookie(suffix, value, expiration) {
coreStorage.setCookie(name + (suffix || ''), value, expiration, 'Lax', domainOverride);
}
}

/**
* @param {SubmoduleContainer} submodule
* @param {(Object|string)} value
Expand All @@ -225,15 +233,15 @@ export function setStoredValue(submodule, value) {
* @type {SubmoduleStorage}
*/
const storage = submodule.config.storage;
const domainOverride = (typeof submodule.submodule.domainOverride === 'function') ? submodule.submodule.domainOverride() : null;

try {
const valueStr = isPlainObject(value) ? JSON.stringify(value) : value;
const expiresStr = (new Date(Date.now() + (storage.expires * (60 * 60 * 24 * 1000)))).toUTCString();
const valueStr = isPlainObject(value) ? JSON.stringify(value) : value;
if (storage.type === COOKIE) {
coreStorage.setCookie(storage.name, valueStr, expiresStr, 'Lax', domainOverride);
const setCookie = cookieSetter(submodule);
setCookie(null, value, expiresStr);
if (typeof storage.refreshInSeconds === 'number') {
coreStorage.setCookie(`${storage.name}_last`, new Date().toUTCString(), expiresStr, 'Lax', domainOverride);
setCookie('_last', new Date().toUTCString(), expiresStr);
}
} else if (storage.type === LOCAL_STORAGE) {
coreStorage.setDataInLocalStorage(`${storage.name}_exp`, expiresStr);
Expand All @@ -247,6 +255,31 @@ export function setStoredValue(submodule, value) {
}
}

export function deleteStoredValue(submodule) {
let deleter, suffixes;
switch (submodule.config?.storage?.type) {
case COOKIE:
const setCookie = cookieSetter(submodule);
const expiry = (new Date(Date.now() - 1000 * 60 * 60 * 24)).toUTCString();
deleter = (suffix) => setCookie(suffix, '', expiry)
suffixes = ['', '_last'];
break;
case LOCAL_STORAGE:
deleter = (suffix) => coreStorage.removeDataFromLocalStorage(submodule.config.storage.name + suffix)
suffixes = ['', '_last', '_exp'];
break;
}
if (deleter) {
suffixes.forEach(suffix => {
try {
deleter(suffix)
} catch (e) {
logError(e);
}
});
}
}

function setPrebidServerEidPermissions(initializedSubmodules) {
let setEidPermissions = getPrebidInternal().setEidPermissions;
if (typeof setEidPermissions === 'function' && isArray(initializedSubmodules)) {
Expand Down Expand Up @@ -1002,12 +1035,28 @@ function updateSubmodules() {
if (!addedUserIdHook && submodules.length) {
// priority value 40 will load after consentManagement with a priority of 50
getGlobal().requestBids.before(requestBidsHook, 40);
adapterManager.callDataDeletionRequest.before(requestDataDeletion);
coreGetPPID.after((next) => next(getPPID()));
logInfo(`${MODULE_NAME} - usersync config updated for ${submodules.length} submodules: `, submodules.map(a => a.submodule.name));
addedUserIdHook = true;
}
}

export function requestDataDeletion(next, ...args) {
logInfo('UserID: received data deletion request; deleting all stored IDs...')
submodules.forEach(submodule => {
if (typeof submodule.submodule.onDataDeletionRequest === 'function') {
try {
submodule.submodule.onDataDeletionRequest(submodule.config, submodule.idObj, ...args);
} catch (e) {
logError(`Error calling onDataDeletionRequest for ID submodule ${submodule.submodule.name}`, e);
}
}
deleteStoredValue(submodule);
})
next.apply(this, args);
}

/**
* enable submodule in User ID
* @param {Submodule} submodule
Expand Down
63 changes: 56 additions & 7 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {GdprConsentHandler, UspConsentHandler} from './consentHandler.js';
import * as events from './events.js';
import CONSTANTS from './constants.json';
import {useMetrics} from './utils/perfMetrics.js';
import {auctionManager} from './auctionManager.js';

export const PARTITIONS = {
CLIENT: 'client',
Expand Down Expand Up @@ -553,19 +554,30 @@ adapterManager.getAnalyticsAdapter = function(code) {
return _analyticsRegistry[code];
}

function tryCallBidderMethod(bidder, method, param) {
function getBidderMethod(bidder, method) {
const adapter = _bidderRegistry[bidder];
const spec = adapter?.getSpec && adapter.getSpec();
if (spec && spec[method] && typeof spec[method] === 'function') {
return [spec, spec[method]]
}
}

function invokeBidderMethod(bidder, method, spec, fn, ...params) {
try {
const adapter = _bidderRegistry[bidder];
const spec = adapter.getSpec();
if (spec && spec[method] && typeof spec[method] === 'function') {
logInfo(`Invoking ${bidder}.${method}`);
config.runWithBidder(bidder, bind.call(spec[method], spec, param));
}
logInfo(`Invoking ${bidder}.${method}`);
config.runWithBidder(bidder, fn.bind(spec, ...params));
} catch (e) {
logWarn(`Error calling ${method} of ${bidder}`);
}
}

function tryCallBidderMethod(bidder, method, param) {
const target = getBidderMethod(bidder, method);
if (target != null) {
invokeBidderMethod(bidder, method, ...target, param);
}
}

adapterManager.callTimedOutBidders = function(adUnits, timedOutBidders, cbTimeout) {
timedOutBidders = timedOutBidders.map((timedOutBidder) => {
// Adding user configured params & timeout to timeout event data
Expand Down Expand Up @@ -600,4 +612,41 @@ adapterManager.callBidderError = function(bidder, error, bidderRequest) {
tryCallBidderMethod(bidder, 'onBidderError', param);
};

function resolveAlias(alias) {
const seen = new Set();
while (_aliasRegistry.hasOwnProperty(alias) && !seen.has(alias)) {
seen.add(alias);
alias = _aliasRegistry[alias];
}
return alias;
}
/**
* Ask every adapter to delete PII.
* See https://github.com/prebid/Prebid.js/issues/9081
*/
adapterManager.callDataDeletionRequest = hook('sync', function (...args) {
const method = 'onDataDeletionRequest';
Object.keys(_bidderRegistry)
.filter((bidder) => !_aliasRegistry.hasOwnProperty(bidder))
.forEach(bidder => {
const target = getBidderMethod(bidder, method);
if (target != null) {
const bidderRequests = auctionManager.getBidsRequested().filter((br) =>
resolveAlias(br.bidderCode) === bidder
);
invokeBidderMethod(bidder, method, ...target, bidderRequests, ...args);
}
});
Object.entries(_analyticsRegistry).forEach(([name, entry]) => {
const fn = entry?.adapter?.[method];
if (typeof fn === 'function') {
try {
fn.apply(entry.adapter, args);
} catch (e) {
logError(`error calling ${method} of ${name}`, e);
}
}
});
});

export default adapterManager;
Loading

0 comments on commit 90db169

Please sign in to comment.