From 60b6821b78076bb3334d2bdcac41d86b1d2566fe Mon Sep 17 00:00:00 2001 From: Aparna Hegde Date: Fri, 3 Aug 2018 12:36:14 -0400 Subject: [PATCH 1/4] check gdpr in buildRequest --- modules/33acrossBidAdapter.js | 23 +- test/spec/modules/33acrossBidAdapter_spec.js | 348 ++++++++++++------- 2 files changed, 247 insertions(+), 124 deletions(-) diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 7b26f66331f..7cfb0eb224e 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -24,7 +24,7 @@ function _createBidResponse(response) { } // infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request -function _createServerRequest(bidRequest) { +function _createServerRequest(bidRequest, gdprConsent) { const ttxRequest = {}; const params = bidRequest.params; @@ -48,11 +48,24 @@ function _createServerRequest(bidRequest) { // therefore in ad targetting process ttxRequest.id = bidRequest.bidId; + // Set GDPR related fields + ttxRequest.user = { + ext: { + consent: gdprConsent.consentString + } + } + ttxRequest.regs = { + ext: { + gdpr: (gdprConsent.gdprApplies === true) ? 1 : 0 + } + } + // Finally, set the openRTB 'test' param if this is to be a test bid if (params.test === 1) { ttxRequest.test = 1; } + /* * Now construct the full server request */ @@ -105,10 +118,14 @@ function isBidRequestValid(bid) { } // NOTE: At this point, TTX only accepts request for a single impression -function buildRequests(bidRequests) { +function buildRequests(bidRequests, bidderRequest) { + const gdprConsent = Object.assign({ consentString: undefined, gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent) + adapterState.uniqueSiteIds = bidRequests.map(req => req.params.siteId).filter(uniques); - return bidRequests.map(_createServerRequest); + return bidRequests.map((req) => { + return _createServerRequest(req, gdprConsent); + }); } // NOTE: At this point, the response from 33exchange will only ever contain one bid i.e. the highest bid diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 8ca2f3da902..1be5f115430 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -40,6 +40,7 @@ describe('33acrossBidAdapter:', function () { afterEach(function() { this.sandbox.restore(); + delete this.bidRequests; }); describe('isBidRequestValid:', function () { @@ -112,150 +113,255 @@ describe('33acrossBidAdapter:', function () { }); describe('buildRequests:', function() { - it('returns corresponding server requests for each valid bidRequest', function() { - const ttxRequest = { - imp: [ { - banner: { - format: [ - { - w: 300, - h: 250, - ext: {} - }, - { - w: 728, - h: 90, - ext: {} + + context('when gdpr consent data exists', function() { + beforeEach(function() { + this.bidderRequest= { + gdprConsent: { + consentString: "foobarMyPreference", + gdprApplies: true + } + } + }); + + it('returns corresponding server requests with gdpr consent data', function() { + const ttxRequest = { + imp: [ { + banner: { + format: [ + { + w: 300, + h: 250, + ext: {} + }, + { + w: 728, + h: 90, + ext: {} + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID } - ] + } + } ], + site: { + id: SITE_ID }, - ext: { - ttx: { - prod: PRODUCT_ID + id: 'b1', + user: { + ext: { + consent: "foobarMyPreference" + } + }, + regs: { + ext: { + gdpr: 1 } } - } ], - site: { - id: SITE_ID - }, - id: 'b1' - }; - const serverRequest = { - 'method': 'POST', - 'url': END_POINT, - 'data': JSON.stringify(ttxRequest), - 'options': { - 'contentType': 'text/plain', - 'withCredentials': true - } - } - const builtServerRequests = buildRequests(this.bidRequests); - expect(builtServerRequests).to.deep.equal([ serverRequest ]); - expect(builtServerRequests.length).to.equal(1); - }); + }; - it('returns corresponding test server requests for each valid bidRequest', function() { - this.sandbox.stub(config, 'getConfig').callsFake(() => { - return { - 'url': 'https://foo.com/hb/' + const serverRequest = { + 'method': 'POST', + 'url': END_POINT, + 'data': JSON.stringify(ttxRequest), + 'options': { + 'contentType': 'text/plain', + 'withCredentials': true + } } + const builtServerRequests = buildRequests(this.bidRequests, this.bidderRequest); + expect(builtServerRequests).to.deep.equal([ serverRequest ]); + expect(builtServerRequests.length).to.equal(1); }); - const ttxRequest = { - imp: [ { - banner: { - format: [ - { - w: 300, - h: 250, - ext: { } - }, - { - w: 728, - h: 90, - ext: { } + it('returns corresponding test server requests with gdpr consent data', function() { + this.sandbox.stub(config, 'getConfig').callsFake(() => { + return { + 'url': 'https://foo.com/hb/' + } + }); + + const ttxRequest = { + imp: [ { + banner: { + format: [ + { + w: 300, + h: 250, + ext: { } + }, + { + w: 728, + h: 90, + ext: { } + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID } - ] + } + } ], + site: { + id: SITE_ID }, - ext: { - ttx: { - prod: PRODUCT_ID + id: 'b1', + user: { + ext: { + consent: "foobarMyPreference" + } + }, + regs: { + ext: { + gdpr: 1 } } - } ], - site: { - id: SITE_ID - }, - id: 'b1' - }; - const serverRequest = { - method: 'POST', - url: 'https://foo.com/hb/', - data: JSON.stringify(ttxRequest), - options: { - contentType: 'text/plain', - withCredentials: true - } - }; + }; + const serverRequest = { + method: 'POST', + url: 'https://foo.com/hb/', + data: JSON.stringify(ttxRequest), + options: { + contentType: 'text/plain', + withCredentials: true + } + }; - const builtServerRequests = buildRequests(this.bidRequests); - expect(builtServerRequests).to.deep.equal([ serverRequest ]); - expect(builtServerRequests.length).to.equal(1); + const builtServerRequests = buildRequests(this.bidRequests, this.bidderRequest); + expect(builtServerRequests).to.deep.equal([ serverRequest ]); + expect(builtServerRequests.length).to.equal(1); + }); + + afterEach(function() { + delete this.bidderRequest; + }) }); - it('returns corresponding test server requests for each valid bidRequest', function() { - this.sandbox.stub(config, 'getConfig').callsFake(() => { - return { - 'url': 'https://foo.com/hb/' - } + context('when gdpr consent data does not exist', function() { + beforeEach(function() { + this.bidderRequest= { } }); - this.bidRequests[0].params.test = 1; - const ttxRequest = { - imp: [ { - banner: { - format: [ - { - w: 300, - h: 250, - ext: { } - }, - { - w: 728, - h: 90, - ext: { } + + it('returns corresponding server requests with default gdpr consent data', function() { + const ttxRequest = { + imp: [ { + banner: { + format: [ + { + w: 300, + h: 250, + ext: {} + }, + { + w: 728, + h: 90, + ext: {} + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID } - ] + } + } ], + site: { + id: SITE_ID }, - ext: { - ttx: { - prod: PRODUCT_ID + id: 'b1', + user: { + ext: { + consent: undefined + } + }, + regs: { + ext: { + gdpr: 0 } } - } ], - site: { - id: SITE_ID - }, - id: 'b1', - test: 1 - }; - const serverRequest = { - method: 'POST', - url: 'https://foo.com/hb/', - data: JSON.stringify(ttxRequest), - options: { - contentType: 'text/plain', - withCredentials: true + }; + + const serverRequest = { + 'method': 'POST', + 'url': END_POINT, + 'data': JSON.stringify(ttxRequest), + 'options': { + 'contentType': 'text/plain', + 'withCredentials': true + } } - }; + const builtServerRequests = buildRequests(this.bidRequests, this.bidderRequest); + expect(builtServerRequests).to.deep.equal([ serverRequest ]); + expect(builtServerRequests.length).to.equal(1); + }); - const builtServerRequests = buildRequests(this.bidRequests); - expect(builtServerRequests).to.deep.equal([ serverRequest ]); - expect(builtServerRequests.length).to.equal(1); - }); + it('returns corresponding test server requests with default gdpr consent data', function() { + this.sandbox.stub(config, 'getConfig').callsFake(() => { + return { + 'url': 'https://foo.com/hb/' + } + }); + + const ttxRequest = { + imp: [ { + banner: { + format: [ + { + w: 300, + h: 250, + ext: { } + }, + { + w: 728, + h: 90, + ext: { } + } + ] + }, + ext: { + ttx: { + prod: PRODUCT_ID + } + } + } ], + site: { + id: SITE_ID + }, + id: 'b1', + user: { + ext: { + consent: undefined + } + }, + regs: { + ext: { + gdpr: 0 + } + } + }; + const serverRequest = { + method: 'POST', + url: 'https://foo.com/hb/', + data: JSON.stringify(ttxRequest), + options: { + contentType: 'text/plain', + withCredentials: true + } + }; - afterEach(function() { - delete this.bidRequests; - }) + const builtServerRequests = buildRequests(this.bidRequests, this.bidderRequest); + expect(builtServerRequests).to.deep.equal([ serverRequest ]); + expect(builtServerRequests.length).to.equal(1); + }); + + afterEach(function() { + delete this.bidderRequest; + }) + }); }); describe('interpretResponse', function() { From 888a1bdd96883bc773d5dc804e1ac2a55fcb0ed5 Mon Sep 17 00:00:00 2001 From: Aparna Hegde Date: Fri, 3 Aug 2018 14:58:08 -0400 Subject: [PATCH 2/4] User sync based on whether gdpr applies or not --- modules/33acrossBidAdapter.js | 17 ++++++-- test/spec/modules/33acrossBidAdapter_spec.js | 43 ++++++++++++++------ 2 files changed, 43 insertions(+), 17 deletions(-) diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 7cfb0eb224e..6f356a5d3f4 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -23,7 +23,8 @@ function _createBidResponse(response) { } } -// infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request +// Infer the necessary data from valid bid for a minimal ttxRequest and create HTTP request +// NOTE: At this point, TTX only accepts request for a single impression function _createServerRequest(bidRequest, gdprConsent) { const ttxRequest = {}; const params = bidRequest.params; @@ -117,7 +118,10 @@ function isBidRequestValid(bid) { return true; } -// NOTE: At this point, TTX only accepts request for a single impression +// NOTE: With regards to gdrp consent data, +// - the server independently infers gdpr applicability therefore, setting the default value to false +// - the server, at this point, also doesn't need the consent string to handle gdpr compliance. So passing +// value whether set or not, for the sake of future dev. function buildRequests(bidRequests, bidderRequest) { const gdprConsent = Object.assign({ consentString: undefined, gdprApplies: false }, bidderRequest && bidderRequest.gdprConsent) @@ -141,8 +145,13 @@ function interpretResponse(serverResponse, bidRequest) { } // Register one sync per unique guid -function getUserSyncs(syncOptions) { - return (syncOptions.iframeEnabled) ? adapterState.uniqueSiteIds.map(_createSync) : ([]); +// NOTE: If gdpr applies do not sync +function getUserSyncs(syncOptions, responses, gdprConsent) { + if (typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { + return [] + } else { + return (syncOptions.iframeEnabled) ? adapterState.uniqueSiteIds.map(_createSync) : ([]); + } } const spec = { diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 1be5f115430..57130339fdf 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -558,23 +558,40 @@ describe('33acrossBidAdapter:', function () { ]; }); - context('when iframe is not enabled', function() { - it('returns empty sync array', function() { - const syncOptions = {}; - buildRequests(this.bidRequests); - expect(getUserSyncs(syncOptions)).to.deep.equal([]); + context('when gdpr does not apply', function() { + beforeEach(function() { + this.gdprConsent = {} + }); + + context('when iframe is not enabled', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + buildRequests(this.bidRequests); + expect(getUserSyncs(syncOptions, {}, this.gdprConsent)).to.deep.equal([]); + }); + }); + + context('when iframe is enabled', function() { + it('returns sync array equal to number of unique siteIDs', function() { + const syncOptions = { + iframeEnabled: true + }; + buildRequests(this.bidRequests); + const syncs = getUserSyncs(syncOptions, {}, this.gdprConsent); + expect(syncs).to.deep.equal(this.syncs); + }); }); }); - context('when iframe is enabled', function() { - it('returns sync array equal to number of unique siteIDs', function() { - const syncOptions = { - iframeEnabled: true - }; + context('when gdpr applies', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + const gdprConsent = { + gdprApplies: true + } buildRequests(this.bidRequests); - const syncs = getUserSyncs(syncOptions); - expect(syncs).to.deep.equal(this.syncs); + expect(getUserSyncs(syncOptions, {}, gdprConsent)).to.deep.equal([]); }); - }); + }) }); }); From 59f8304c54492a029267a9c99c6c0a33c85a55d3 Mon Sep 17 00:00:00 2001 From: Aparna Hegde Date: Fri, 3 Aug 2018 17:35:07 -0400 Subject: [PATCH 3/4] check if consent data exists during user sync --- modules/33acrossBidAdapter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/33acrossBidAdapter.js b/modules/33acrossBidAdapter.js index 6f356a5d3f4..109d9387ce6 100644 --- a/modules/33acrossBidAdapter.js +++ b/modules/33acrossBidAdapter.js @@ -147,7 +147,7 @@ function interpretResponse(serverResponse, bidRequest) { // Register one sync per unique guid // NOTE: If gdpr applies do not sync function getUserSyncs(syncOptions, responses, gdprConsent) { - if (typeof gdprConsent.gdprApplies === 'boolean' && gdprConsent.gdprApplies) { + if (gdprConsent && gdprConsent.gdprApplies === true) { return [] } else { return (syncOptions.iframeEnabled) ? adapterState.uniqueSiteIds.map(_createSync) : ([]); From 1096846800dd96ca4e61076582d2602300a1012a Mon Sep 17 00:00:00 2001 From: Aparna Hegde Date: Tue, 7 Aug 2018 13:31:20 -0400 Subject: [PATCH 4/4] split user sync into further branches: 1) when gdpr does not apply 2) when consent data is unavailable --- test/spec/modules/33acrossBidAdapter_spec.js | 25 +++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/test/spec/modules/33acrossBidAdapter_spec.js b/test/spec/modules/33acrossBidAdapter_spec.js index 57130339fdf..3dbd7401d76 100644 --- a/test/spec/modules/33acrossBidAdapter_spec.js +++ b/test/spec/modules/33acrossBidAdapter_spec.js @@ -560,7 +560,9 @@ describe('33acrossBidAdapter:', function () { context('when gdpr does not apply', function() { beforeEach(function() { - this.gdprConsent = {} + this.gdprConsent = { + gdprApplies: false + } }); context('when iframe is not enabled', function() { @@ -583,6 +585,27 @@ describe('33acrossBidAdapter:', function () { }); }); + context('when consent data is not defined', function() { + context('when iframe is not enabled', function() { + it('returns empty sync array', function() { + const syncOptions = {}; + buildRequests(this.bidRequests); + expect(getUserSyncs(syncOptions)).to.deep.equal([]); + }); + }); + + context('when iframe is enabled', function() { + it('returns sync array equal to number of unique siteIDs', function() { + const syncOptions = { + iframeEnabled: true + }; + buildRequests(this.bidRequests); + const syncs = getUserSyncs(syncOptions); + expect(syncs).to.deep.equal(this.syncs); + }); + }); + }); + context('when gdpr applies', function() { it('returns empty sync array', function() { const syncOptions = {};