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

TCFv2.0 Purpose 7 #5444

Merged
merged 34 commits into from
Aug 13, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
3042c1c
TCF v2.0 enforcement
Fawke Apr 30, 2020
73d3b52
test/spec/modules/gdprEnforcement_spec.js
Fawke Apr 30, 2020
a49f7e1
add check for gdpr version
Fawke May 5, 2020
559724e
add logInfo message
Fawke May 5, 2020
d945a8a
remove comment and store value of PURPOSES in an object
Fawke May 7, 2020
06e46d2
add gvlid check
Fawke May 11, 2020
f1fcf31
merge with master - change in validateRules function
Fawke May 28, 2020
cc57db8
add unit tests for validateRules function
Fawke Jun 2, 2020
9b98e98
remove purposeId parameter from validateRules function
Fawke Jun 2, 2020
87cfe20
merge with master
Fawke Jun 4, 2020
859d782
add extra tests
Fawke Jun 4, 2020
4e3978c
merge with master
Fawke Jun 4, 2020
1a0f5a7
make failing unit test case pass
Fawke Jun 4, 2020
ae9a237
deprecate allowAuctionWithouConsent with tcf 2 workflow
Fawke Jun 5, 2020
82e80a8
add extra checks for defaults
Fawke Jun 5, 2020
5311bac
remove tcf 2 test page
Fawke Jun 5, 2020
358a556
add strict gvlid check
Fawke Jun 8, 2020
d584a35
add comments and shorten log messages
Fawke Jun 9, 2020
0b461fb
shorted log messages
Fawke Jun 9, 2020
32c0682
add unit tests for setEnforcementConfig
Fawke Jun 9, 2020
35a6729
Merge remote-tracking branch 'origin/prebid-4.0' into tcf-purpose2
Jun 13, 2020
e7f14c0
add gvlid for alias and gvlMapping support
Jun 15, 2020
d0abaf0
remove gvlid check
Fawke Jun 15, 2020
45e9fb6
add support to add gvlid for aliases
Jun 17, 2020
beb7b65
Merge branch 'tcf-purpose2' of github.com:prebid/Prebid.js into tcf-p…
Jun 17, 2020
f95e876
add enableAnalytics hook
Fawke Jun 23, 2020
7700d42
purpose 7 implementation: 1.hook added 2.new field to set gvlid for a…
Fawke Jul 1, 2020
abf357b
add enableAnalytics hook
Fawke Jul 6, 2020
e683df6
emit tcf2 events
Fawke Jul 7, 2020
ee932d5
fix regression
Fawke Jul 7, 2020
ee60177
modify mechanism of event emitted after auction end
Fawke Jul 8, 2020
1c99028
add unit test for enableAnalyticsHook
Fawke Jul 8, 2020
d2e5311
merge with master
Fawke Aug 10, 2020
02fa662
add unit test for auction end event
Fawke Aug 12, 2020
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
3 changes: 2 additions & 1 deletion modules/appnexusAnalyticsAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ var appnexusAdapter = adapter({

adapterManager.registerAnalyticsAdapter({
adapter: appnexusAdapter,
code: 'appnexus'
code: 'appnexus',
gvlid: 32
});

export default appnexusAdapter;
119 changes: 109 additions & 10 deletions modules/gdprEnforcement.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ import { EVENTS } from '../src/constants.json';

const TCF2 = {
'purpose1': { id: 1, name: 'storage' },
'purpose2': { id: 2, name: 'basicAds' }
'purpose2': { id: 2, name: 'basicAds' },
'purpose7': { id: 7, name: 'measurement' }
}

/*
These rules would be used if `consentManagement.gdpr.rules` is undefined by the publisher.
*/
const DEFAULT_RULES = [{
purpose: 'storage',
enforcePurpose: true,
Expand All @@ -33,9 +37,21 @@ const DEFAULT_RULES = [{

export let purpose1Rule;
export let purpose2Rule;
let addedDeviceAccessHook = false;
export let purpose7Rule;

export let enforcementRules;

const storageBlocked = [];
const biddersBlocked = [];
const analyticsBlocked = [];

let addedDeviceAccessHook = false;

/**
* Returns gvlId for Bid Adapters. If a bidder does not have an associated gvlId, it returns 'undefined'.
* @param {string=} bidderCode - The 'code' property on the Bidder spec.
* @retuns {number} gvlId
*/
function getGvlid(bidderCode) {
let gvlid;
bidderCode = bidderCode || config.getCurrentBidder();
Expand All @@ -53,6 +69,11 @@ function getGvlid(bidderCode) {
return gvlid;
}

/**
* Returns gvlId for userId module. If a userId modules does not have an associated gvlId, it returns 'undefined'.
* @param {Object} userIdModule
* @retuns {number} gvlId
*/
function getGvlidForUserIdModule(userIdModule) {
let gvlId;
const gvlMapping = config.getConfig('gvlMapping');
Expand All @@ -64,6 +85,22 @@ function getGvlidForUserIdModule(userIdModule) {
return gvlId;
}

/**
* Returns gvlId for analytics adapters. If a analytics adapter does not have an associated gvlId, it returns 'undefined'.
* @param {string} code - 'provider' property on the analytics adapter config
* @returns {number} gvlId
*/
function getGvlidForAnalyticsAdapter(code) {
let gvlId;
const gvlMapping = config.getConfig('gvlMapping');
if (gvlMapping && gvlMapping[code]) {
gvlId = gvlMapping[code];
} else {
gvlId = adapterManager.getAnalyticsAdapter(code).gvlid;
}
return gvlId;
}

/**
* This function takes in a rule and consentData and validates against the consentData provided. Depending on what it returns,
* the caller may decide to suppress a TCF-sensitive activity.
Expand Down Expand Up @@ -136,8 +173,9 @@ export function deviceAccessHook(fn, gvlid, moduleName, result) {
result.valid = true;
fn.call(this, gvlid, moduleName, result);
} else {
curModule && utils.logWarn(`Device access denied for ${curModule} by TCF2`);
curModule && utils.logWarn(`TCF2 denied device access for ${curModule}`);
result.valid = false;
storageBlocked.push(curModule);
fn.call(this, gvlid, moduleName, result);
}
} else {
Expand Down Expand Up @@ -168,6 +206,7 @@ export function userSyncHook(fn, ...args) {
fn.call(this, ...args);
} else {
utils.logWarn(`User sync not allowed for ${curBidder}`);
storageBlocked.push(curBidder);
}
} else {
// The module doesn't enforce TCF1.1 strings
Expand Down Expand Up @@ -195,10 +234,11 @@ export function userIdHook(fn, submodules, consentData) {
return submodule;
} else {
utils.logWarn(`User denied permission to fetch user id for ${moduleName} User id module`);
storageBlocked.push(moduleName);
}
return undefined;
}).filter(module => module)
fn.call(this, userIdModules, {...consentData, hasValidated: true});
fn.call(this, userIdModules, { ...consentData, hasValidated: true });
} else {
// The module doesn't enforce TCF1.1 strings
fn.call(this, submodules, consentData);
Expand All @@ -209,26 +249,24 @@ export function userIdHook(fn, submodules, consentData) {
}

/**
* Checks if a bidder is allowed in Auction.
* Enforces "purpose 2 (basic ads)" of TCF v2.0 spec
* Checks if bidders are allowed in the auction.
* Enforces "purpose 2 (Basic Ads)" of TCF v2.0 spec
* @param {Function} fn - Function reference to the original function.
* @param {Array<adUnits>} adUnits
*/
export function makeBidRequestsHook(fn, adUnits, ...args) {
const consentData = gdprDataHandler.getConsentData();
if (consentData && consentData.gdprApplies) {
if (consentData.apiVersion === 2) {
const disabledBidders = [];
adUnits.forEach(adUnit => {
adUnit.bids = adUnit.bids.filter(bid => {
const currBidder = bid.bidder;
const gvlId = getGvlid(currBidder);
if (includes(disabledBidders, currBidder)) return false;
if (includes(biddersBlocked, currBidder)) return false;
const isAllowed = !!validateRules(purpose2Rule, consentData, currBidder, gvlId);
if (!isAllowed) {
utils.logWarn(`TCF2 blocked auction for ${currBidder}`);
events.emit(EVENTS.BIDDER_BLOCKED, currBidder);
disabledBidders.push(currBidder);
biddersBlocked.push(currBidder);
}
return isAllowed;
});
Expand All @@ -243,8 +281,64 @@ export function makeBidRequestsHook(fn, adUnits, ...args) {
}
}

/**
* Checks if Analytics adapters are allowed to send data to their servers for furhter processing.
* Enforces "purpose 7 (Measurement)" of TCF v2.0 spec
* @param {Function} fn - Function reference to the original function.
* @param {Array<AnalyticsAdapterConfig>} config - Configuration object passed to pbjs.enableAnalytics()
*/
export function enableAnalyticsHook(fn, config) {
const consentData = gdprDataHandler.getConsentData();
if (consentData && consentData.gdprApplies) {
if (consentData.apiVersion === 2) {
if (!utils.isArray(config)) {
config = [config]
}
config = config.filter(conf => {
const analyticsAdapterCode = conf.provider;
const gvlid = getGvlidForAnalyticsAdapter(analyticsAdapterCode);
const isAllowed = !!validateRules(purpose7Rule, consentData, analyticsAdapterCode, gvlid);
if (!isAllowed) {
analyticsBlocked.push(analyticsAdapterCode);
utils.logWarn(`TCF2 blocked analytics adapter ${conf.provider}`);
}
return isAllowed;
});
fn.call(this, config);
} else {
// This module doesn't enforce TCF1.1 strings
fn.call(this, config);
}
} else {
fn.call(this, config);
}
}

/**
* Compiles the TCF2.0 enforcement results into an object, which is emitted as an event payload to "tcf2Enforcement" event.
*/
function emitTCF2FinalResults() {
// remove null and duplicate values
const formatArray = function (arr) {
return arr.filter((i, k) => i !== null && arr.indexOf(i) === k);
}
const tcf2FinalResults = {
storageBlocked: formatArray(storageBlocked),
biddersBlocked: formatArray(biddersBlocked),
analyticsBlocked: formatArray(analyticsBlocked)
};

events.emit(EVENTS.TCF2_ENFORCEMENT, tcf2FinalResults);
}

events.on(EVENTS.AUCTION_END, emitTCF2FinalResults);

/*
Set of callback functions used to detect presence of a TCF rule, passed as the second argument to find().
*/
const hasPurpose1 = (rule) => { return rule.purpose === TCF2.purpose1.name }
const hasPurpose2 = (rule) => { return rule.purpose === TCF2.purpose2.name }
const hasPurpose7 = (rule) => { return rule.purpose === TCF2.purpose7.name }

/**
* A configuration function that initializes some module variables, as well as adds hooks
Expand All @@ -261,6 +355,7 @@ export function setEnforcementConfig(config) {

purpose1Rule = find(enforcementRules, hasPurpose1);
purpose2Rule = find(enforcementRules, hasPurpose2);
purpose7Rule = find(enforcementRules, hasPurpose7);

if (!purpose1Rule) {
purpose1Rule = DEFAULT_RULES[0];
Expand All @@ -280,6 +375,10 @@ export function setEnforcementConfig(config) {
if (purpose2Rule) {
getHook('makeBidRequests').before(makeBidRequestsHook);
}

if (purpose7Rule) {
getHook('enableAnalyticsCb').before(enableAnalyticsHook);
}
}

config.getConfig('consentManagement', config => setEnforcementConfig(config.consentManagement));
12 changes: 8 additions & 4 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -468,11 +468,11 @@ adapterManager.aliasBidAdapter = function (bidderCode, alias, options) {
}
};

adapterManager.registerAnalyticsAdapter = function ({adapter, code}) {
adapterManager.registerAnalyticsAdapter = function ({adapter, code, gvlid}) {
if (adapter && code) {
if (typeof adapter.enableAnalytics === 'function') {
adapter.code = code;
_analyticsRegistry[code] = adapter;
_analyticsRegistry[code] = { adapter, gvlid };
} else {
utils.logError(`Prebid Error: Analytics adaptor error for analytics "${code}"
analytics adapter must implement an enableAnalytics() function`);
Expand All @@ -488,20 +488,24 @@ adapterManager.enableAnalytics = function (config) {
}

utils._each(config, adapterConfig => {
var adapter = _analyticsRegistry[adapterConfig.provider];
var adapter = _analyticsRegistry[adapterConfig.provider].adapter;
if (adapter) {
adapter.enableAnalytics(adapterConfig);
} else {
utils.logError(`Prebid Error: no analytics adapter found in registry for
${adapterConfig.provider}.`);
}
});
};
}

adapterManager.getBidAdapter = function(bidder) {
return _bidderRegistry[bidder];
};

adapterManager.getAnalyticsAdapter = function(code) {
return _analyticsRegistry[code];
}

// the s2sTesting module is injected when it's loaded rather than being imported
// importing it causes the packager to include it even when it's not explicitly included in the build
export function setS2STestingModule(module) {
Expand Down
4 changes: 2 additions & 2 deletions src/constants.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
"BEFORE_REQUEST_BIDS": "beforeRequestBids",
"REQUEST_BIDS": "requestBids",
"ADD_AD_UNITS": "addAdUnits",
"AD_RENDER_FAILED" : "adRenderFailed",
"BIDDER_BLOCKED": "bidderBlocked"
"AD_RENDER_FAILED": "adRenderFailed",
"TCF2_ENFORCEMENT": "tcf2Enforcement"
},
"AD_RENDER_FAILED_REASON" : {
"PREVENT_WRITING_ON_MAIN_DOCUMENT": "preventWritingOnMainDocuemnt",
Expand Down
15 changes: 12 additions & 3 deletions src/prebid.js
Original file line number Diff line number Diff line change
Expand Up @@ -536,8 +536,9 @@ $$PREBID_GLOBAL$$.requestBids = hook('async', function ({ bidsBackHandler, timeo
auction.callBids();
});

export function executeStorageCallbacks(fn, reqBidsConfigObj) {
export function executeCallbacks(fn, reqBidsConfigObj) {
runAll(storageCallbacks);
runAll(enableAnalyticsCallbacks);
fn.call(this, reqBidsConfigObj);
function runAll(queue) {
var queued;
Expand All @@ -548,7 +549,7 @@ export function executeStorageCallbacks(fn, reqBidsConfigObj) {
}

// This hook will execute all storage callbacks which were registered before gdpr enforcement hook was added. Some bidders, user id modules use storage functions when module is parsed but gdpr enforcement hook is not added at that stage as setConfig callbacks are yet to be called. Hence for such calls we execute all the stored callbacks just before requestBids. At this hook point we will know for sure that gdprEnforcement module is added or not
$$PREBID_GLOBAL$$.requestBids.before(executeStorageCallbacks, 49);
$$PREBID_GLOBAL$$.requestBids.before(executeCallbacks, 49);

/**
*
Expand Down Expand Up @@ -667,13 +668,21 @@ $$PREBID_GLOBAL$$.createBid = function (statusCode) {
* @param {Object} config.options The options for this particular analytics adapter. This will likely vary between adapters.
* @alias module:pbjs.enableAnalytics
*/
$$PREBID_GLOBAL$$.enableAnalytics = function (config) {

// Stores 'enableAnalytics' callbacks for later execution.
const enableAnalyticsCallbacks = [];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome stuff 👍


const enableAnalyticsCb = hook('async', function (config) {
if (config && !utils.isEmpty(config)) {
utils.logInfo('Invoking $$PREBID_GLOBAL$$.enableAnalytics for: ', config);
adapterManager.enableAnalytics(config);
} else {
utils.logError('$$PREBID_GLOBAL$$.enableAnalytics should be called with option {}');
}
}, 'enableAnalyticsCb');

$$PREBID_GLOBAL$$.enableAnalytics = function (config) {
enableAnalyticsCallbacks.push(enableAnalyticsCb.bind(this, config));
};

/**
Expand Down
Loading