Skip to content

Commit

Permalink
consentManagementGpp: pause auctions when user is reviewing / updatin…
Browse files Browse the repository at this point in the history
…g consent preferences (#12224)
  • Loading branch information
dgirardi authored Sep 19, 2024
1 parent 24516d3 commit 38ecf2a
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 42 deletions.
19 changes: 14 additions & 5 deletions modules/consentManagementGpp.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {gppDataHandler} from '../src/adapterManager.js';
import {enrichFPD} from '../src/fpd/enrichment.js';
import {getGlobal} from '../src/prebidGlobal.js';
import {cmpClient, MODE_CALLBACK} from '../libraries/cmp/cmpClient.js';
import {GreedyPromise} from '../src/utils/promise.js';
import {GreedyPromise, defer} from '../src/utils/promise.js';
import {buildActivityParams} from '../src/activities/params.js';
import {consentManagementHook} from '../libraries/consentManagement/cmUtils.js';

Expand Down Expand Up @@ -72,7 +72,7 @@ export class GPPClient {

constructor(cmp) {
this.cmp = cmp;
[this.#resolve, this.#reject] = [0, 1].map(slot => (result) => {
[this.#resolve, this.#reject] = ['resolve', 'reject'].map(slot => (result) => {
while (this.#pending.length) {
this.#pending.pop()[slot](result);
}
Expand Down Expand Up @@ -103,6 +103,15 @@ export class GPPClient {
} else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) {
this.#resolve(this.updateConsent(event.pingData));
}
// NOTE: according to https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md,
// > [signalStatus] Event is called whenever the display status of the CMP changes (e.g. the CMP shows the consent layer).
//
// however, from real world testing, at least some CMPs only trigger 'cmpDisplayStatus'
// other CMPs may do something else yet; here we just look for 'signalStatus: not ready' on any event
// to decide if consent data is likely to change
if (consentData != null && event?.pingData != null && !this.isCMPReady(event.pingData)) {
consentData = null;
}
}
});
}
Expand Down Expand Up @@ -136,9 +145,9 @@ export class GPPClient {
* @returns {Promise<{}>}
*/
nextUpdate() {
return new GreedyPromise((resolve, reject) => {
this.#pending.push([resolve, reject]);
});
const def = defer();
this.#pending.push(def);
return def.promise;
}

/**
Expand Down
92 changes: 55 additions & 37 deletions test/spec/modules/consentManagementGpp_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ import {gppDataHandler} from 'src/adapterManager.js';
import * as utils from 'src/utils.js';
import {config} from 'src/config.js';
import 'src/prebid.js';
import {MODE_CALLBACK, MODE_MIXED} from '../../../libraries/cmp/cmpClient.js';
import {GreedyPromise} from '../../../src/utils/promise.js';

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

Expand Down Expand Up @@ -348,8 +346,8 @@ describe('consentManagementGpp', function () {
sinon.assert.match(gppDataHandler.getConsentData(), gppData2);
});
});
})
})
});
});
});
});

Expand Down Expand Up @@ -496,56 +494,76 @@ describe('consentManagementGpp', function () {
});
});

describe('already known consentData:', function () {
let cmpStub = sinon.stub();

function mockCMP(pingData) {
return function (command, callback) {
describe('on CMP sectionChange events', () => {
let pingData, triggerCMPEvent;
beforeEach(() => {
pingData = {
applicableSections: [7],
gppString: 'xyz',
};
triggerCMPEvent = null;
window.__gpp = sinon.stub().callsFake(function (command, callback) {
switch (command) {
case 'addEventListener':
// eslint-disable-next-line standard/no-callback-literal
callback({eventName: 'sectionChange', pingData})
triggerCMPEvent = (event, payload = {}) => callback({eventName: event, pingData: {...pingData, ...payload}})
break;
case 'ping':
callback(pingData)
break;
default:
throw new Error('unexpected __gpp invocation')
}
}
}

beforeEach(function () {
didHookReturn = false;
window.__gpp = function () {};
});
setConsentConfig(goodConfig);
});

afterEach(function () {
config.resetConfig();
cmpStub.restore();
afterEach(() => {
delete window.__gpp;
resetConsentData();
});

it('should bypass CMP and simply use previously stored consentData', function () {
let testConsentData = {
applicableSections: [7],
gppString: 'xyz',
};

cmpStub = sinon.stub(window, '__gpp').callsFake(mockCMP({...testConsentData, signalStatus: 'ready'}));
setConsentConfig(goodConfig);
requestBidsHook(() => {}, {});
cmpStub.reset();

function startHook() {
let hookRan = false;
requestBidsHook(() => {
didHookReturn = true;
hookRan = true;
}, {});
let consent = gppDataHandler.getConsentData();
return () => new Promise((resolve) => setTimeout(resolve(hookRan), 5));
}

expect(didHookReturn).to.be.true;
expect(consent.gppString).to.equal(testConsentData.gppString);
expect(consent.applicableSections).to.deep.equal(testConsentData.applicableSections);
sinon.assert.notCalled(cmpStub);
it('should wait for signalStatus: ready', async () => {
const didHookRun = startHook();
expect(await didHookRun()).to.be.false;
triggerCMPEvent('sectionChange', {signalStatus: 'not ready'});
expect(await didHookRun()).to.be.false;
triggerCMPEvent('sectionChange', {signalStatus: 'ready'});
expect(await didHookRun()).to.be.true;
expect(gppDataHandler.getConsentData().gppString).to.eql('xyz');
});
});

it('should re-use GPP data once ready', async () => {
let didHookRun = startHook();
triggerCMPEvent('sectionChange', {signalStatus: 'ready'});
await didHookRun();
window.__gpp.reset();
didHookRun = startHook();
expect(await didHookRun()).to.be.true;
sinon.assert.notCalled(window.__gpp);
});

it('after signalStatus: ready, should wait again for signalStatus: ready', async () => {
let didHookRun = startHook();
triggerCMPEvent('sectionChange', {signalStatus: 'ready'});
await didHookRun();
for (let run of ['first', 'second']) {
triggerCMPEvent('cmpDisplayStatus', {signalStatus: 'not ready'});
didHookRun = startHook();
expect(await didHookRun()).to.be.false;
triggerCMPEvent('sectionChange', {signalStatus: 'ready', gppString: run});
expect(await didHookRun()).to.be.true;
expect(gppDataHandler.getConsentData().gppString).to.eql(run);
}
});
})
});
});

0 comments on commit 38ecf2a

Please sign in to comment.