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

GDPR - add consent information to PBS cookie_sync request #2530

Merged
merged 2 commits into from
May 15, 2018
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
61 changes: 41 additions & 20 deletions modules/prebidServerBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,36 +96,56 @@ function setS2sConfig(options) {
}

_s2sConfig = options;
if (options.syncEndpoint) {
queueSync(options.bidders);
}
}
getConfig('s2sConfig', ({s2sConfig}) => setS2sConfig(s2sConfig));

/**
* resets the _synced variable back to false, primiarily used for testing purposes
*/
export function resetSyncedStatus() {
_synced = false;
}

/**
* @param {Array} bidderCodes list of bidders to request user syncs for.
*/
function queueSync(bidderCodes) {
function queueSync(bidderCodes, gdprConsent) {
Copy link
Member

Choose a reason for hiding this comment

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

Would be awesome if this code could be reused for spec.getUserSyncs but it's ok for now.

if (_synced) {
return;
}
_synced = true;
const payload = JSON.stringify({

let payload = {
Copy link
Contributor

Choose a reason for hiding this comment

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

why const => let here? I see mutation, but no re-assignments... seems like const should still work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll change it to const to be consistent.

Copy link
Contributor

@dbemiller dbemiller May 15, 2018

Choose a reason for hiding this comment

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

Sounds good.

If you have a choice, always use const instead of let. There are a million little principles out there which make code slightly easier to read, and they add up when you put them all together... so it's a good habit to have.

uuid: utils.generateUUID(),
bidders: bidderCodes
});
ajax(_s2sConfig.syncEndpoint, (response) => {
try {
response = JSON.parse(response);
response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder));
} catch (e) {
utils.logError(e);
};

if (gdprConsent) {
// only populate gdpr field if we know CMP returned consent information (ie didn't timeout or have an error)
if (gdprConsent.consentString) {
payload.gdpr = (gdprConsent.gdprApplies) ? 1 : 0;
}
},
payload, {
contentType: 'text/plain',
withCredentials: true
});
// attempt to populate gdpr_consent if we know gdprApplies or it may apply
if (gdprConsent.gdprApplies !== false) {
payload.gdpr_consent = gdprConsent.consentString;
}
}
const jsonPayload = JSON.stringify(payload);

ajax(_s2sConfig.syncEndpoint,
(response) => {
try {
response = JSON.parse(response);
response.bidder_status.forEach(bidder => doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder));
} catch (e) {
utils.logError(e);
}
},
jsonPayload,
{
contentType: 'text/plain',
withCredentials: true
});
}

/**
Expand Down Expand Up @@ -348,9 +368,6 @@ const LEGACY_PROTOCOL = {
if (result.status === 'OK' || result.status === 'no_cookie') {
if (result.bidder_status) {
result.bidder_status.forEach(bidder => {
if (bidder.no_cookie) {
doBidderSync(bidder.usersync.type, bidder.usersync.url, bidder.bidder);
}
if (bidder.error) {
utils.logWarn(`Prebid Server returned error: '${bidder.error}' for ${bidder.bidder}`);
}
Expand Down Expand Up @@ -666,6 +683,10 @@ export function PrebidServer() {
.reduce(utils.flatten)
.filter(utils.uniques);

if (_s2sConfig && _s2sConfig.syncEndpoint) {
queueSync(_s2sConfig.bidders, bidRequests[0].gdprConsent);
Copy link
Contributor

Choose a reason for hiding this comment

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

Is bidRequests guaranteed to have at least one element in it? If not, this could break.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Per this check, it should always at least have 1 element:
https://github.com/prebid/Prebid.js/blob/master/src/adaptermanager.js#L289

But I can add another check in this code to be sure.

Copy link
Contributor

@dbemiller dbemiller May 14, 2018

Choose a reason for hiding this comment

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

Either way.

My only two concerns are "does it work now?" and "is it likely that the code changes to break it in the future without anyone realizing?" Looks like the answer to (1) is "yes." (2) usually depends on how thorough the docs and unit tests are.

Your call either way. From my experience, I wouldn't trust the adaptermanager implementation to stay the same unless a unit test existed to enforce it.

}

const request = protocolAdapter().buildRequest(s2sBidRequest, bidRequests, adUnitsWithSizes);
const requestJson = JSON.stringify(request);

Expand Down
83 changes: 55 additions & 28 deletions test/spec/modules/prebidServerBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { expect } from 'chai';
import { PrebidServer as Adapter } from 'modules/prebidServerBidAdapter';
import { PrebidServer as Adapter, resetSyncedStatus } from 'modules/prebidServerBidAdapter';
import adapterManager from 'src/adaptermanager';
import * as utils from 'src/utils';
import cookie from 'src/cookie';
Expand Down Expand Up @@ -392,11 +392,11 @@ describe('S2S Adapter', () => {
expect(requestBid.ad_units[0].bids[0].params.member).to.exist.and.to.be.a('string');
});

it('adds gdpr consent information to ortb2 request depending on module use', () => {
it('adds gdpr consent information to ortb2 request depending on presence of module', () => {
let ortb2Config = utils.deepClone(CONFIG);
ortb2Config.endpoint = 'https://prebid.adnxs.com/pbs/v1/openrtb2/auction'

let consentConfig = { consentManagement: { cmp: 'iab' }, s2sConfig: ortb2Config };
let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: ortb2Config };
config.setConfig(consentConfig);

let gdprBidRequest = utils.deepClone(BID_REQUESTS);
Expand Down Expand Up @@ -424,6 +424,58 @@ describe('S2S Adapter', () => {
$$PREBID_GLOBAL$$.requestBids.removeHook(requestBidsHook);
});

it('check gdpr info gets added into cookie_sync request', () => {
let cookieSyncConfig = utils.deepClone(CONFIG);
cookieSyncConfig.syncEndpoint = 'https://prebid.adnxs.com/pbs/v1/cookie_sync';

let consentConfig = { consentManagement: { cmpApi: 'iab' }, s2sConfig: cookieSyncConfig };
config.setConfig(consentConfig);

let gdprBidRequest = utils.deepClone(BID_REQUESTS);

// check scenario if gdprApplies and we got consent information CMP fine
gdprBidRequest[0].gdprConsent = {
consentString: 'abc123def',
gdprApplies: true
};

adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax);
let requestBid = JSON.parse(requests[0].requestBody);

expect(requestBid.gdpr).is.equal(1);
expect(requestBid.gdpr_consent).is.equal('abc123def');

// check scenario if gdprApplies is false
resetSyncedStatus();
Copy link
Contributor

Choose a reason for hiding this comment

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

Tests shouldn't have side-effects on the global state.

It's also dangerous to put this mid-test because it won't execute if one of the earlier assertions fail.

This test should be split into smaller ones, and resetSyncedStatus() called in an afterEach() block

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

True on the above; I wanted to limit the scenarios where I was using this function to not affect other tests by having that function run for every test. But it doesn't seem like it's breaking the other tests when I put it there now. I'll make the changes here.

Copy link
Contributor

Choose a reason for hiding this comment

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

it's fine... if other tests fail because the global state gets reset, then they're the broken ones. Tests should be initializing their own state, rather than relying on other tests to do it for them.

requests = [];

gdprBidRequest[0].gdprConsent = {
consentString: 'xyz789abcc',
gdprApplies: false
};

adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax);
requestBid = JSON.parse(requests[0].requestBody);

expect(requestBid.gdpr).is.equal(0);
expect(requestBid.gdpr_consent).is.undefined;

// check scenario if we didn't consent information from CMP appropriately (ie timeout)
resetSyncedStatus();
requests = [];

gdprBidRequest[0].gdprConsent = {
consentString: undefined,
gdprApplies: undefined
};

adapter.callBids(REQUEST, gdprBidRequest, addBidResponse, done, ajax);
requestBid = JSON.parse(requests[0].requestBody);

expect(requestBid.gdpr).is.undefined;
expect(requestBid.gdpr_consent).is.undefined;
});

it('sets invalid cacheMarkup value to 0', () => {
const s2sConfig = Object.assign({}, CONFIG, {
cacheMarkup: 999
Expand Down Expand Up @@ -794,31 +846,6 @@ describe('S2S Adapter', () => {
expect(bid_request_passed).to.have.property('adId', '123');
});

it('does cookie sync when no_cookie response', () => {
server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE));

config.setConfig({s2sConfig: CONFIG});
adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax);
server.respond();

sinon.assert.calledOnce(utils.triggerPixel);
sinon.assert.calledWith(utils.triggerPixel, 'https://pixel.rubiconproject.com/exchange/sync.php?p=prebid');
sinon.assert.calledOnce(utils.insertUserSyncIframe);
sinon.assert.calledWith(utils.insertUserSyncIframe, '//ads.pubmatic.com/AdServer/js/user_sync.html?predirect=https%3A%2F%2Fprebid.adnxs.com%2Fpbs%2Fv1%2Fsetuid%3Fbidder%3Dpubmatic%26uid%3D');
});

it('logs error when no_cookie response is missing type or url', () => {
server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE_ERROR));

config.setConfig({s2sConfig: CONFIG});
adapter.callBids(REQUEST, BID_REQUESTS, addBidResponse, done, ajax);
server.respond();

sinon.assert.notCalled(utils.triggerPixel);
sinon.assert.notCalled(utils.insertUserSyncIframe);
sinon.assert.calledTwice(utils.logError);
});

it('does not call cookieSet cookie sync when no_cookie response && not opted in', () => {
server.respondWith(JSON.stringify(RESPONSE_NO_PBS_COOKIE));

Expand Down