From 3f49f21976fe7d9832a42bc553a8eea742d738c0 Mon Sep 17 00:00:00 2001 From: Demetrio Girardi Date: Thu, 27 Jul 2023 10:28:20 -0700 Subject: [PATCH] consent hash & multiHandler --- src/adapterManager.js | 22 +++--- src/consentHandler.js | 73 ++++++++++++++++++-- src/prebid.js | 19 +---- test/spec/unit/core/consentHandler_spec.js | 80 +++++++++++++++++++++- 4 files changed, 161 insertions(+), 33 deletions(-) diff --git a/src/adapterManager.js b/src/adapterManager.js index b600802b522..45c4d890944 100644 --- a/src/adapterManager.js +++ b/src/adapterManager.js @@ -12,7 +12,8 @@ import { getUniqueIdentifierStr, getUserConfiguredParams, groupBy, - isArray, isPlainObject, + isArray, + isPlainObject, isValidMediaTypes, logError, logInfo, @@ -30,7 +31,12 @@ import {hook} from './hook.js'; import {find, includes} from './polyfill.js'; import {adunitCounter} from './adUnits.js'; import {getRefererInfo} from './refererDetection.js'; -import {GDPR_GVLIDS, GdprConsentHandler, GppConsentHandler, UspConsentHandler} from './consentHandler.js'; +import { + GDPR_GVLIDS, + gdprDataHandler, + uspDataHandler, + gppDataHandler, +} from './consentHandler.js'; import * as events from './events.js'; import CONSTANTS from './constants.json'; import {useMetrics} from './utils/perfMetrics.js'; @@ -41,6 +47,8 @@ import {ACTIVITY_FETCH_BIDS, ACTIVITY_REPORT_ANALYTICS} from './activities/activ import {ACTIVITY_PARAM_ANL_CONFIG, ACTIVITY_PARAM_S2S_NAME, activityParamsBuilder} from './activities/params.js'; import {redactor} from './activities/redactor.js'; +export {gdprDataHandler, gppDataHandler, uspDataHandler, coppaDataHandler} from './consentHandler.js'; + export const PBS_ADAPTER_NAME = 'pbsBidAdapter'; export const PARTITIONS = { CLIENT: 'client', @@ -192,16 +200,6 @@ function getAdUnitCopyForClientAdapters(adUnits) { return adUnitsClientCopy; } -export let gdprDataHandler = new GdprConsentHandler(); -export let uspDataHandler = new UspConsentHandler(); -export let gppDataHandler = new GppConsentHandler(); - -export let coppaDataHandler = { - getCoppa: function() { - return !!(config.getConfig('coppa')) - } -}; - /** * Filter and/or modify media types for ad units based on the given labels. * diff --git a/src/consentHandler.js b/src/consentHandler.js index 4776a8ece02..2fd8f0baf35 100644 --- a/src/consentHandler.js +++ b/src/consentHandler.js @@ -1,5 +1,6 @@ -import {isStr, timestamp} from './utils.js'; +import {cyrb53Hash, isStr, timestamp} from './utils.js'; import {defer, GreedyPromise} from './utils/promise.js'; +import {config} from './config.js'; /** * Placeholder gvlid for when vendor consent is not required. When this value is used as gvlid, the gdpr @@ -14,7 +15,10 @@ export class ConsentHandler { #data; #defer; #ready; + #dirty = true; + #hash; generatedTime; + hashFields; constructor() { this.reset(); @@ -74,15 +78,24 @@ export class ConsentHandler { setConsentData(data, time = timestamp()) { this.generatedTime = time; + this.#dirty = true; this.#resolve(data); } getConsentData() { return this.#data; } + + get hash() { + if (this.#dirty) { + this.#hash = cyrb53Hash(JSON.stringify(this.#data && this.hashFields ? this.hashFields.map(f => this.#data[f]) : this.#data)) + this.#dirty = false; + } + return this.#hash; + } } -export class UspConsentHandler extends ConsentHandler { +class UspConsentHandler extends ConsentHandler { getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -94,7 +107,8 @@ export class UspConsentHandler extends ConsentHandler { } } -export class GdprConsentHandler extends ConsentHandler { +class GdprConsentHandler extends ConsentHandler { + hashFields = ['gdprApplies', 'consentString'] getConsentMeta() { const consentData = this.getConsentData(); if (consentData && consentData.vendorData && this.generatedTime) { @@ -108,7 +122,8 @@ export class GdprConsentHandler extends ConsentHandler { } } -export class GppConsentHandler extends ConsentHandler { +class GppConsentHandler extends ConsentHandler { + hashFields = ['applicableSections', 'gppString']; getConsentMeta() { const consentData = this.getConsentData(); if (consentData && this.generatedTime) { @@ -159,4 +174,54 @@ export function gvlidRegistry() { } } +export const gdprDataHandler = new GdprConsentHandler(); +export const uspDataHandler = new UspConsentHandler(); +export const gppDataHandler = new GppConsentHandler(); +export const coppaDataHandler = (() => { + function getCoppa() { + return !!(config.getConfig('coppa')) + } + return { + getCoppa, + getConsentData: getCoppa, + getConsentMeta: getCoppa, + get promise() { + return GreedyPromise.resolve(getCoppa()) + }, + get hash() { + return getCoppa() ? '1' : '0' + } + } +})(); + export const GDPR_GVLIDS = gvlidRegistry(); + +const ALL_HANDLERS = { + gdpr: gdprDataHandler, + usp: uspDataHandler, + gpp: gppDataHandler, + coppa: coppaDataHandler, +} + +export function multiHandler(handlers = ALL_HANDLERS) { + handlers = Object.entries(handlers); + function collector(method) { + return function () { + return Object.fromEntries(handlers.map(([name, handler]) => [name, handler[method]()])) + } + } + return Object.assign( + { + get promise() { + return Promise.all(handlers.map(([name, handler]) => handler.promise.then(val => [name, val]))) + .then(entries => Object.fromEntries(entries)); + }, + get hash() { + return handlers.map(([_, handler]) => handler.hash).join(':'); + } + }, + Object.fromEntries(['getConsentData', 'getConsentMeta'].map(n => [n, collector(n)])), + ) +} + +export const allConsent = multiHandler(); diff --git a/src/prebid.js b/src/prebid.js index b949ece65ea..94ade8e5f83 100644 --- a/src/prebid.js +++ b/src/prebid.js @@ -45,12 +45,13 @@ import {executeRenderer, isRendererRequired} from './Renderer.js'; import {createBid} from './bidfactory.js'; import {storageCallbacks} from './storageManager.js'; import {emitAdRenderFail, emitAdRenderSucceeded} from './adRendering.js'; -import {default as adapterManager, gdprDataHandler, getS2SBidderSet, gppDataHandler, uspDataHandler} from './adapterManager.js'; +import {default as adapterManager, getS2SBidderSet} from './adapterManager.js'; import CONSTANTS from './constants.json'; import * as events from './events.js'; import {newMetrics, useMetrics} from './utils/perfMetrics.js'; import {defer, GreedyPromise} from './utils/promise.js'; import {enrichFPD} from './fpd/enrichment.js'; +import {allConsent} from './consentHandler.js'; const pbjsInstance = getGlobal(); const { triggerUserSyncs } = userSync; @@ -330,23 +331,9 @@ pbjsInstance.getAdserverTargeting = function (adUnitCode) { return targeting.getAllTargeting(adUnitCode); }; -/** - * returns all consent data - * @return {Object} Map of consent types and data - * @alias module:pbjs.getConsentData - */ -function getConsentMetadata() { - return { - gdpr: gdprDataHandler.getConsentMeta(), - usp: uspDataHandler.getConsentMeta(), - gpp: gppDataHandler.getConsentMeta(), - coppa: !!(config.getConfig('coppa')) - } -} - pbjsInstance.getConsentMetadata = function () { logInfo('Invoking $$PREBID_GLOBAL$$.getConsentMetadata'); - return getConsentMetadata(); + return allConsent.getConsentMeta() }; function getBids(type) { diff --git a/test/spec/unit/core/consentHandler_spec.js b/test/spec/unit/core/consentHandler_spec.js index 98b317e0d36..a8e9a9662df 100644 --- a/test/spec/unit/core/consentHandler_spec.js +++ b/test/spec/unit/core/consentHandler_spec.js @@ -1,4 +1,4 @@ -import {ConsentHandler, gvlidRegistry} from '../../../../src/consentHandler.js'; +import {multiHandler, ConsentHandler, gvlidRegistry} from '../../../../src/consentHandler.js'; describe('Consent data handler', () => { let handler; @@ -56,6 +56,84 @@ describe('Consent data handler', () => { }) }) }); + + describe('getHash', () => { + it('is defined when null', () => { + expect(handler.hash).be.a('string'); + }); + it('changes when a field is updated', () => { + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: false}); + const h2 = handler.hash; + expect(h2).to.not.eql(h1); + handler.setConsentData({field: 'value', enabled: true}); + const h3 = handler.hash; + expect(h3).to.not.eql(h2); + expect(h3).to.not.eql(h1); + }); + it('does not change when fields are unchanged', () => { + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true}); + expect(handler.hash).to.eql(h1); + }); + it('does not change when non-hashFields are updated', () => { + handler.hashFields = ['field', 'enabled']; + handler.setConsentData({field: 'value', enabled: true}); + const h1 = handler.hash; + handler.setConsentData({field: 'value', enabled: true, other: 'data'}); + expect(handler.hash).to.eql(h1); + }) + }) +}); + +describe('multiHandler', () => { + let handlers, multi; + beforeEach(() => { + handlers = {h1: {}, h2: {}}; + multi = multiHandler(handlers); + }); + + ['getConsentData', 'getConsentMeta'].forEach(method => { + describe(method, () => { + it('combines results from underlying handlers', () => { + handlers.h1[method] = () => 'one'; + handlers.h2[method] = () => 'two'; + expect(multi[method]()).to.eql({ + h1: 'one', + h2: 'two', + }) + }); + }); + }); + + describe('.promise', () => { + it('resolves all underlying promises', (done) => { + handlers.h1.promise = Promise.resolve('one'); + let resolver, result; + handlers.h2.promise = new Promise((resolve) => { resolver = resolve }); + multi.promise.then((val) => { + result = val; + expect(result).to.eql({ + h1: 'one', + h2: 'two' + }); + done(); + }) + handlers.h1.promise.then(() => { + expect(result).to.not.exist; + resolver('two'); + }); + }) + }); + + describe('.hash', () => { + it('concats underlying hashses', () => { + handlers.h1.hash = 'one'; + handlers.h2.hash = 'two'; + expect(multi.hash).to.eql('one:two'); + }) + }) }) describe('gvlidRegistry', () => {