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

Prebid 9: remove support for GPP 1.0 #11461

Merged
merged 3 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
201 changes: 29 additions & 172 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 {timedAuctionHook} from '../src/utils/perfMetrics.js';
import {enrichFPD} from '../src/fpd/enrichment.js';
import {getGlobal} from '../src/prebidGlobal.js';
import {cmpClient, MODE_CALLBACK, MODE_MIXED, MODE_RETURN} from '../libraries/cmp/cmpClient.js';
import {cmpClient, MODE_CALLBACK} from '../libraries/cmp/cmpClient.js';
import {GreedyPromise} from '../src/utils/promise.js';
import {buildActivityParams} from '../src/activities/params.js';

Expand Down Expand Up @@ -38,9 +38,6 @@ function lookupStaticConsentData(callbacks) {
return pipeCallbacks(() => processCmpData(staticConsentData), callbacks);
}

const GPP_10 = '1.0';
const GPP_11 = '1.1';

class GPPError {
constructor(message, arg) {
this.message = message;
Expand All @@ -49,104 +46,22 @@ class GPPError {
}

export class GPPClient {
static CLIENTS = {};

static register(apiVersion, defaultVersion = false) {
this.apiVersion = apiVersion;
this.CLIENTS[apiVersion] = this;
if (defaultVersion) {
this.CLIENTS.default = this;
}
}

apiVersion = '1.1';
static INST;

/**
* Ping the CMP to set up an appropriate client for it, and initialize it.
*
* @param mkCmp
* @returns {Promise<[GPPClient,Promise<{}>]>} a promise to two objects:
* - a GPPClient that talks the best GPP dialect we know for the CMP's version;
* - a promise to GPP data.
*/
static init(mkCmp = cmpClient) {
let inst = this.INST;
if (!inst) {
let err;
const reset = () => err && (this.INST = null);
inst = this.INST = this.ping(mkCmp).catch(e => {
err = true;
reset();
throw e;
static get(mkCmp = cmpClient) {
if (this.INST == null) {
const cmp = mkCmp({
apiName: '__gpp',
apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use),
mode: MODE_CALLBACK
});
reset();
}
return inst.then(([client, pingData]) => [
client,
client.initialized ? client.refresh() : client.init(pingData)
]);
}

/**
* Ping the CMP to determine its version and set up a client appropriate for it.
*
* @param mkCmp
* @returns {Promise<[GPPClient, {}]>} a promise to two objects:
* - a GPPClient that talks the best GPP dialect we know for the CMP's version;
* - the result from pinging the CMP.
*/
static ping(mkCmp = cmpClient) {
const cmpOptions = {
apiName: '__gpp',
apiArgs: ['command', 'callback', 'parameter'], // do not pass version - not clear what it's for (or what we should use)
};

// in 1.0, 'ping' should return pingData but ignore callback;
// in 1.1 it should not return anything but run the callback
// the following looks for either - but once the version is known, produce a client that knows whether the
// rest of the interactions should pick return values or pass callbacks

const probe = mkCmp({...cmpOptions, mode: MODE_RETURN});
return new GreedyPromise((resolve, reject) => {
if (probe == null) {
reject(new GPPError('GPP CMP not found'));
return;
if (cmp == null) {
throw new GPPError('GPP CMP not found');
}
let done = false; // some CMPs do both return value and callbacks - avoid repeating log messages
const pong = (result, success) => {
if (done) return;
if (success != null && !success) {
reject(result);
return;
}
if (result == null) return;
done = true;
const cmpVersion = result?.gppVersion;
const Client = this.getClient(cmpVersion);
if (cmpVersion !== Client.apiVersion) {
logWarn(`Unrecognized GPP CMP version: ${cmpVersion}. Continuing using GPP API version ${Client}...`);
} else {
logInfo(`Using GPP version ${cmpVersion}`);
}
const mode = Client.apiVersion === GPP_10 ? MODE_MIXED : MODE_CALLBACK;
const client = new Client(
cmpVersion,
mkCmp({...cmpOptions, mode})
);
resolve([client, result]);
};

probe({
command: 'ping',
callback: pong
}).then((res) => pong(res, true), reject);
}).finally(() => {
probe && probe.close();
});
}

static getClient(cmpVersion) {
return this.CLIENTS.hasOwnProperty(cmpVersion) ? this.CLIENTS[cmpVersion] : this.CLIENTS.default;
this.INST = new this(cmp);
}
return this.INST;
}

#resolve;
Expand All @@ -155,9 +70,7 @@ export class GPPClient {

initialized = false;

constructor(cmpVersion, cmp) {
this.apiVersion = this.constructor.apiVersion;
this.cmpVersion = cmp;
constructor(cmp) {
this.cmp = cmp;
[this.#resolve, this.#reject] = [0, 1].map(slot => (result) => {
while (this.#pending.length) {
Expand All @@ -176,6 +89,9 @@ export class GPPClient {
init(pingData) {
const ready = this.updateWhenReady(pingData);
if (!this.initialized) {
if (pingData.gppVersion !== this.apiVersion) {
logWarn(`Unrecognized GPP CMP version: ${pingData.apiVersion}. Continuing using GPP API version ${this.apiVersion}...`);
}
this.initialized = true;
this.cmp({
command: 'addEventListener',
Expand All @@ -184,7 +100,7 @@ export class GPPClient {
this.#reject(new GPPError('Received error response from CMP', event));
} else if (event?.pingData?.cmpStatus === 'error') {
this.#reject(new GPPError('CMP status is "error"; please check CMP setup', event));
} else if (this.isCMPReady(event?.pingData || {}) && this.events.includes(event?.eventName)) {
} else if (this.isCMPReady(event?.pingData || {}) && ['sectionChange', 'signalStatus'].includes(event?.eventName)) {
this.#resolve(this.updateConsent(event.pingData));
}
}
Expand All @@ -194,7 +110,7 @@ export class GPPClient {
}

refresh() {
return this.cmp({command: 'ping'}).then(this.updateWhenReady.bind(this));
return this.cmp({command: 'ping'}).then(this.init.bind(this));
}

/**
Expand All @@ -204,15 +120,14 @@ export class GPPClient {
* @returns {Promise<{}>} a promise to GPP consent data
*/
updateConsent(pingData) {
return this.getGPPData(pingData).then((data) => {
if (data == null || isEmpty(data)) {
throw new GPPError('Received empty response from CMP', data);
return new GreedyPromise(resolve => {
if (pingData == null || isEmpty(pingData)) {
throw new GPPError('Received empty response from CMP', pingData);
}
return processCmpData(data);
}).then((data) => {
logInfo('Retrieved GPP consent from CMP:', data);
return data;
});
const consentData = processCmpData(pingData);
logInfo('Retrieved GPP consent from CMP:', consentData);
resolve(consentData);
})
}

/**
Expand All @@ -236,68 +151,10 @@ export class GPPClient {
updateWhenReady(pingData) {
return this.isCMPReady(pingData) ? this.updateConsent(pingData) : this.nextUpdate();
}
}

// eslint-disable-next-line no-unused-vars
class GPP10Client extends GPPClient {
static {
super.register(GPP_10);
}

events = ['sectionChange', 'cmpStatus'];

isCMPReady(pingData) {
return pingData.cmpStatus === 'loaded';
}

getGPPData(pingData) {
const parsedSections = GreedyPromise.all(
(pingData.supportedAPIs || pingData.apiSupport || []).map((api) => this.cmp({
command: 'getSection',
parameter: api
}).catch(err => {
logWarn(`Could not retrieve GPP section '${api}'`, err);
}).then((section) => [api, section]))
).then(sections => {
// parse single section object into [core, gpc] to uniformize with 1.1 parsedSections
return Object.fromEntries(
sections.filter(([_, val]) => val != null)
.map(([api, section]) => {
const subsections = [
Object.fromEntries(Object.entries(section).filter(([k]) => k !== 'Gpc'))
];
if (section.Gpc != null) {
subsections.push({
SubsectionType: 1,
Gpc: section.Gpc
});
}
return [api, subsections];
})
);
});
return GreedyPromise.all([
this.cmp({command: 'getGPPData'}),
parsedSections
]).then(([gppData, parsedSections]) => Object.assign({}, gppData, {parsedSections}));
}
}

// eslint-disable-next-line no-unused-vars
class GPP11Client extends GPPClient {
static {
super.register(GPP_11, true);
}

events = ['sectionChange', 'signalStatus'];

isCMPReady(pingData) {
return pingData.signalStatus === 'ready';
}

getGPPData(pingData) {
return GreedyPromise.resolve(pingData);
}
}

/**
Expand All @@ -308,7 +165,7 @@ class GPP11Client extends GPPClient {
* @param {function(string, ...{}?)} cmpError acts as an error callback while interacting with CMP; pass along an error message (string) and any extra error arguments (purely for logging)
*/
export function lookupIabConsent({onSuccess, onError}, mkCmp = cmpClient) {
pipeCallbacks(() => GPPClient.init(mkCmp).then(([client, gppDataPm]) => gppDataPm), {onSuccess, onError});
pipeCallbacks(() => GPPClient.get(mkCmp).refresh(), {onSuccess, onError});
}

// add new CMPs here, with their dedicated lookup function
Expand Down Expand Up @@ -425,9 +282,9 @@ function processCmpData(consentData) {
}
['usnatv1', 'uscav1'].forEach(section => {
if (consentData?.parsedSections?.[section]) {
logWarn(`Received invalid section from cmp: '${section}'. Some functionality may not work as expected`, consentData)
logWarn(`Received invalid section from cmp: '${section}'. Some functionality may not work as expected`, consentData);
}
})
});
return storeConsentData(consentData);
}

Expand Down
Loading