Skip to content

Commit

Permalink
Prebid core: add support for asynchronous access to consent data
Browse files Browse the repository at this point in the history
This adds `gpdrDataHandler.promise` and `uspDataHandler.promise`, to enable access to USP/GDPR consent data from outside of an auction context (see use case: prebid#7803)
  • Loading branch information
dgirardi committed Feb 15, 2022
1 parent eb06300 commit 15e5464
Show file tree
Hide file tree
Showing 7 changed files with 180 additions and 23 deletions.
5 changes: 4 additions & 1 deletion modules/consentManagement.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) {

if (!includes(Object.keys(cmpCallMap), userCMP)) {
logWarn(`CMP framework (${userCMP}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
gdprDataHandler.setConsentData(null);
return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args);
}

Expand Down Expand Up @@ -452,6 +453,7 @@ function exitModule(errMsg, hookConfig, extraArgs) {
nextFn.apply(context, args);
} else {
logError(errMsg + ' Canceling auction as per consentManagement config.', extraArgs);
gdprDataHandler.setConsentData(null);
if (typeof hookConfig.bidsBackHandler === 'function') {
hookConfig.bidsBackHandler();
} else {
Expand All @@ -471,7 +473,7 @@ export function resetConsentData() {
consentData = undefined;
userCMP = undefined;
cmpVersion = 0;
gdprDataHandler.setConsentData(null);
gdprDataHandler.reset();
}

/**
Expand Down Expand Up @@ -509,6 +511,7 @@ export function setConsentConfig(config) {
gdprScope = config.defaultGdprScope === true;

logInfo('consentManagement module has been activated...');
gdprDataHandler.enable();

if (userCMP === 'static') {
if (isPlainObject(config.consentData)) {
Expand Down
5 changes: 4 additions & 1 deletion modules/consentManagementUsp.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export function requestBidsHook(fn, reqBidsConfigObj) {

if (!uspCallMap[consentAPI]) {
logWarn(`USP framework (${consentAPI}) is not a supported framework. Aborting consentManagement module and resuming auction.`);
uspDataHandler.setConsentData(null);
return hookConfig.nextFn.apply(hookConfig.context, hookConfig.args);
}

Expand Down Expand Up @@ -276,6 +277,7 @@ function exitModule(errMsg, hookConfig, extraArgs) {

if (errMsg) {
logWarn(errMsg + ' Resuming auction without consent data as per consentManagement config.', extraArgs);
uspDataHandler.setConsentData(null) // let core know that no consent data is available
}
nextFn.apply(context, args);
}
Expand All @@ -287,7 +289,7 @@ function exitModule(errMsg, hookConfig, extraArgs) {
export function resetConsentData() {
consentData = undefined;
consentAPI = undefined;
uspDataHandler.setConsentData(null);
uspDataHandler.reset();
}

/**
Expand Down Expand Up @@ -315,6 +317,7 @@ export function setConsentConfig(config) {
}

logInfo('USPAPI consentManagement module has been activated...');
uspDataHandler.enable();

if (consentAPI === 'static') {
if (isPlainObject(config.consentData) && isPlainObject(config.consentData.getUSPData)) {
Expand Down
22 changes: 3 additions & 19 deletions src/adapterManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import includes from 'core-js-pure/features/array/includes.js';
import find from 'core-js-pure/features/array/find.js';
import { adunitCounter } from './adUnits.js';
import { getRefererInfo } from './refererDetection.js';
import {ConsentHandler} from './consentHandler.js';

var CONSTANTS = require('./constants.json');
var events = require('./events.js');
Expand Down Expand Up @@ -171,25 +172,8 @@ function getAdUnitCopyForClientAdapters(adUnits) {
return adUnitsClientCopy;
}

export let gdprDataHandler = {
consentData: null,
setConsentData: function(consentInfo) {
gdprDataHandler.consentData = consentInfo;
},
getConsentData: function() {
return gdprDataHandler.consentData;
}
};

export let uspDataHandler = {
consentData: null,
setConsentData: function(consentInfo) {
uspDataHandler.consentData = consentInfo;
},
getConsentData: function() {
return uspDataHandler.consentData;
}
};
export let gdprDataHandler = new ConsentHandler();
export let uspDataHandler = new ConsentHandler();

export let coppaDataHandler = {
getCoppa: function() {
Expand Down
68 changes: 68 additions & 0 deletions src/consentHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@

export class ConsentHandler {
#enabled;
#data;
#promise;
#resolve;
#ready;

constructor() {
this.reset();
}

/**
* reset this handler (mainly for tests)
*/
reset() {
this.#promise = new Promise((resolve) => {
this.#resolve = (data) => {
this.#ready = true;
this.#data = data;
resolve(data);
};
});
this.#enabled = false;
this.#data = null;
this.#ready = false;
}

/**
* Enable this consent handler. This should be called by the relevant consent management module
* on initialization.
*/
enable() {
this.#enabled = true;
}

/**
* @returns {boolean} true if the related consent management module is enabled.
*/
get enabled() {
return this.#enabled;
}

/**
* @returns {boolean} true if consent data has been resolved (it may be `null` if the resolution failed).
*/
get ready() {
return this.#ready;
}

/**
* @returns a promise than resolves to the consent data, or null if no consent data is available
*/
get promise() {
if (!this.#enabled) {
this.#resolve(null);
}
return this.#promise;
}

setConsentData(data) {
this.#resolve(data);
}

getConsentData() {
return this.#data;
}
}
45 changes: 43 additions & 2 deletions test/spec/modules/consentManagementUsp_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import {
} from 'modules/consentManagementUsp.js';
import * as utils from 'src/utils.js';
import { config } from 'src/config.js';
import { uspDataHandler } from 'src/adapterManager.js';
import {gdprDataHandler, uspDataHandler} from 'src/adapterManager.js';
import 'src/prebid.js';

let assert = require('chai').assert;
let expect = require('chai').expect;

function createIFrameMarker() {
Expand Down Expand Up @@ -91,6 +91,21 @@ describe('consentManagement', function () {
expect(consentAPI).to.be.equal('daa');
expect(consentTimeout).to.be.equal(7500);
});

it('should enable uspDataHandler', () => {
setConsentConfig({usp: {cmpApi: 'daa', timeout: 7500}});
expect(uspDataHandler.enabled).to.be.true;
});

it('should call setConsentData(null) on invalid CMP api', () => {
setConsentConfig({usp: {cmpApi: 'invalid'}});
let hookRan = false;
requestBidsHook(() => {
hookRan = true;
}, {});
expect(hookRan).to.be.true;
expect(uspDataHandler.ready).to.be.true;
});
});

describe('static consent string setConsentConfig value', () => {
Expand Down Expand Up @@ -220,6 +235,32 @@ describe('consentManagement', function () {
expect(consent).to.equal(testConsentData.uspString);
sinon.assert.called(uspStub);
});

it('should call uspDataHandler.setConsentData(null) on error', () => {
let hookRan = false;
uspStub = sinon.stub(window, '__uspapi').callsFake((...args) => {
args[2](null, false);
});
requestBidsHook(() => {
hookRan = true;
}, {});
expect(hookRan).to.be.true;
expect(uspDataHandler.ready).to.be.true;
expect(uspDataHandler.getConsentData()).to.equal(null);
});

it('should call uspDataHandler.setConsentData(null) on timeout', (done) => {
setConsentConfig({usp: {timeout: 10}});
let hookRan = false;
uspStub = sinon.stub(window, '__uspapi').callsFake(() => {});
requestBidsHook(() => { hookRan = true; }, {});
setTimeout(() => {
expect(hookRan).to.be.true;
expect(uspDataHandler.ready).to.be.true;
expect(uspDataHandler.getConsentData()).to.equal(null);
done();
}, 20)
});
});

describe('USPAPI workflow for iframed page', function () {
Expand Down
17 changes: 17 additions & 0 deletions test/spec/modules/consentManagement_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { setConsentConfig, requestBidsHook, resetConsentData, userCMP, consentTi
import { gdprDataHandler } from 'src/adapterManager.js';
import * as utils from 'src/utils.js';
import { config } from 'src/config.js';
import 'src/prebid.js';

let expect = require('chai').expect;

Expand Down Expand Up @@ -124,6 +125,11 @@ describe('consentManagement', function () {
});
expect(gdprScope).to.be.equal(false);
});

it('should enable gdprDataHandler', () => {
setConsentConfig({gdpr: {}});
expect(gdprDataHandler.enabled).to.be.true;
});
});

describe('static consent string setConsentConfig value', () => {
Expand Down Expand Up @@ -318,6 +324,14 @@ describe('consentManagement', function () {
expect(consent).to.be.null;
});

it('should call gpdrDataHandler.setConsentData() when unknown CMP api is used', () => {
setConsentConfig({gdpr: {cmpApi: 'invalid'}});
let hookRan = false;
requestBidsHook(() => { hookRan = true; }, {});
expect(hookRan).to.be.true;
expect(gdprDataHandler.ready).to.be.true;
})

it('should throw proper errors when CMP is not found', function () {
setConsentConfig(goodConfigWithCancelAuction);

Expand All @@ -329,6 +343,7 @@ describe('consentManagement', function () {
sinon.assert.calledTwice(utils.logError);
expect(didHookReturn).to.be.false;
expect(consent).to.be.null;
expect(gdprDataHandler.ready).to.be.true;
});
});

Expand Down Expand Up @@ -713,6 +728,7 @@ describe('consentManagement', function () {
expect(didHookReturn).to.be.false;
expect(bidsBackHandlerReturn).to.be.true;
expect(consent).to.be.null;
expect(gdprDataHandler.ready).to.be.true;
});

it('allows the auction when CMP is unresponsive', (done) => {
Expand All @@ -731,6 +747,7 @@ describe('consentManagement', function () {
const consent = gdprDataHandler.getConsentData();
expect(consent.gdprApplies).to.be.true;
expect(consent.consentString).to.be.undefined;
expect(gdprDataHandler.ready).to.be.true;
done();
}, 20);
});
Expand Down
41 changes: 41 additions & 0 deletions test/spec/unit/core/consentHandler_spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {ConsentHandler} from '../../../../src/consentHandler.js';

describe('Consent data handler', () => {
let handler;
beforeEach(() => {
handler = new ConsentHandler();
})

it('should be disabled, return null data on init', () => {
expect(handler.enabled).to.be.false;
expect(handler.getConsentData()).to.equal(null);
})

it('should resolve promise to null when disabled', () => {
return handler.promise.then((data) => {
expect(data).to.equal(null);
});
});

it('should return data after setConsentData', () => {
const data = {consent: 'string'};
handler.enable();
handler.setConsentData(data);
expect(handler.getConsentData()).to.equal(data);
});

it('should resolve .promise to data after setConsentData', (done) => {
let actual = null;
const data = {consent: 'string'};
handler.enable();
handler.promise.then((d) => actual = d);
setTimeout(() => {
expect(actual).to.equal(null);
handler.setConsentData(data);
setTimeout(() => {
expect(actual).to.equal(data);
done();
})
})
});
})

0 comments on commit 15e5464

Please sign in to comment.