From 559630ea621887157e8ce61894cb1c80950c5209 Mon Sep 17 00:00:00 2001 From: Yevhenii Tykhostup Date: Fri, 17 Jul 2020 16:48:44 +0300 Subject: [PATCH 1/4] Add apstream adapter --- modules/apstreamBidAdapter.js | 225 ++++++++++++++ modules/apstreamBidAdapter.md | 85 ++++++ test/spec/modules/apstreamBidAdapter_spec.js | 306 +++++++++++++++++++ 3 files changed, 616 insertions(+) create mode 100644 modules/apstreamBidAdapter.js create mode 100644 modules/apstreamBidAdapter.md create mode 100644 test/spec/modules/apstreamBidAdapter_spec.js diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js new file mode 100644 index 00000000000..cc98565e2b5 --- /dev/null +++ b/modules/apstreamBidAdapter.js @@ -0,0 +1,225 @@ +import { registerBidder } from '../src/adapters/bidderFactory.js'; +import { config } from '../src/config.js'; +import * as utils from '../src/utils.js'; +import { getStorageManager } from '../src/storageManager.js'; + +const CONSTANTS = { + DSU_KEY: 'apr_dsu', + BIDDER_CODE: 'apstream', + GVLID: 394 +}; +const storage = getStorageManager(CONSTANTS.GVLID, CONSTANTS.BIDDER_CODE); + +function serializeSizes(sizes) { + if (Array.isArray(sizes[0]) === false) { + sizes = [sizes]; + } + + return sizes.map(s => s[0] + 'x' + s[1]).join('_'); +} + +function getRawConsentString(gdprConsentConfig) { + if (!gdprConsentConfig || gdprConsentConfig.gdprApplies === false) { + return null; + } + + return gdprConsentConfig.consentString; +} + +function getConsentStringFromPrebid(gdprConsentConfig) { + const consentString = getRawConsentString(gdprConsentConfig); + if (!consentString) { + return null; + } + + let isIab = config.getConfig('consentManagement.cmpApi') != 'static'; + let vendorConsents = ( + gdprConsentConfig.vendorData.vendorConsents || + (gdprConsentConfig.vendorData.vendor || {}).consents || + {} + ); + let isConsentGiven = !!vendorConsents[CONSTANTS.GVLID.toString(10)]; + + return isIab && isConsentGiven ? consentString : null; +} + +function getIabConsentString(bidderRequest) { + if (utils.deepAccess(bidderRequest, 'gdprConsent')) { + return getConsentStringFromPrebid(bidderRequest.gdprConsent); + } + + return null; +} + +function injectPixels(ad, pixels, scripts) { + if (!pixels && !scripts) { + return ad; + } + + let trackedAd = ad; + if (pixels) { + pixels.forEach(pixel => { + const tracker = utils.createTrackPixelHtml(pixel); + trackedAd += tracker; + }); + } + + if (scripts) { + scripts.forEach(script => { + const tracker = ``; + trackedAd += tracker; + }); + } + + return trackedAd; +} + +function getScreenParams() { + return `${window.screen.width}x${window.screen.height}@${window.devicePixelRatio}`; +} + +function getBids(bids) { + const bidArr = bids.map(bid => { + const bidId = bid.bidId; + + let mediaType = ''; + const mediaTypes = Object.keys(bid.mediaTypes) + switch (mediaTypes[0]) { + case 'video': + mediaType = 'v'; + break; + + case 'native': + mediaType = 'n'; + break; + + case 'audio': + mediaType = 'a'; + break; + + default: + mediaType = 'b'; + break; + } + + let adUnitCode = `,c=${bid.adUnitCode}`; + if (bid.params.code) { + adUnitCode = `,c=${encodeURIComponent(bid.params.code)}`; + } + if (bid.params.adunitId) { + adUnitCode = `,u=${encodeURIComponent(bid.params.adunitId)}`; + } + + return `${bidId}:t=${mediaType},s=${serializeSizes(bid.sizes)}${adUnitCode}`; + }); + + return bidArr.join(';'); +}; + +function getEndpointsGroups(bidRequests) { + let endpoints = []; + const getEndpoint = bid => { + if (bid.params.test) { + return `https://mock-bapi.userreport.com/v2/${bid.params.publisherId}/bid`; + } + + if (bid.params.endpoint) { + return `${bid.params.endpoint}${bid.params.publisherId}/bid`; + } + + return `https://bapi.userreport.com/v2/${bid.params.publisherId}/bid`; + } + bidRequests.forEach(bid => { + const exist = endpoints.filter(item => item.endpoint.indexOf(bid.params.endpoint) > -1)[0]; + if (exist) { + exist.bids.push(bid); + } else { + endpoints.push({ + endpoint: getEndpoint(bid), + bids: [bid] + }); + } + }); + + return endpoints; +} + +function isBidRequestValid(bid) { + const isPublisherIdExist = !!bid.params.publisherId; + const isOneMediaType = Object.keys(bid.mediaTypes).length === 1; + + return isPublisherIdExist && isOneMediaType; +} + +function buildRequests(bidRequests, bidderRequest) { + const data = { + med: encodeURIComponent(window.location.href), + auid: bidderRequest.auctionId, + ref: document.referrer, + dnt: Number(window.navigator.doNotTrack) || 0, + sr: getScreenParams() + }; + + const consentData = getRawConsentString(bidderRequest.gdprConsent); + data.iab_consent = consentData; + + let options = { + withCredentials: true + }; + + const isConsent = getIabConsentString(bidderRequest); + if (isConsent) { + // eslint-disable-next-line + const dsuModule=function(){"use strict";var n="apr_dsu",r="1",e="YicAu6ZpNG",t={USERREPORT:"1"};function o(n,r){var e=n.l+r.l,t={h:n.h+r.h+(e/2>>>31)>>>0,l:e>>>0};n.h=t.h,n.l=t.l}function x(n,r){n.h^=r.h,n.h>>>=0,n.l^=r.l,n.l>>>=0}function a(n,r){var e={h:n.h<>>32-r,l:n.l<>>32-r};n.h=e.h,n.l=e.l}function u(n){var r=n.l;n.l=n.h,n.h=r}function i(n,r,e,t){o(n,r),o(e,t),a(r,13),a(t,16),x(r,n),x(t,e),u(n),o(e,r),o(n,t),a(r,17),a(t,21),x(r,e),x(t,n),u(e)}function l(n,r){return n[r+3]<<24|n[r+2]<<16|n[r+1]<<8|n[r]}function c(n,r){"string"==typeof r&&(r=function(n){if("function"==typeof TextEncoder)return(new TextEncoder).encode(n);n=unescape(encodeURIComponent(n));for(var r=new Uint8Array(n.length),e=0,t=n.length;e>>0,l:n[0]>>>0},t={h:n[3]>>>0,l:n[2]>>>0},o={h:e.h,l:e.l},a=e,u={h:t.h,l:t.l},c=t,f=r.length,h=f-7,s=new Uint8Array(new ArrayBuffer(8));x(o,{h:1936682341,l:1886610805}),x(u,{h:1685025377,l:1852075885}),x(a,{h:1819895653,l:1852142177}),x(c,{h:1952801890,l:2037671283});for(var d=0;dn.indexOf("/")),e=h.exec(r?"noscheme://"+n:n),t={scheme:r?"":e[2]||"",host:e[4]||"",hostname:e[4]?e[4].split(":")[0]:"",pathname:e[5]||"",search:e[7]||"",hash:e[9]||"",toString:function(){return n}};return t.origin=t.scheme+"://"+t.host,t}(n);return d.url=n,d.parsed=r,r}function p(){var n=function(){if("function"==typeof Uint32Array&&"undefined"!=typeof crypto&&void 0!==crypto.getRandomValues){var n=new Uint32Array(4);crypto.getRandomValues(n);var r=-1;return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=n[++r>>3]>>r%8*4&15;return("x"===e?t:3&t|8).toString(16)})}var e=(new Date).getTime();return"undefined"!=typeof performance&&"function"==typeof performance.now&&(e+=performance.now()),"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(n){var r=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"===n?r:3&r|8).toString(16)})}(),o=v(),x=f(n+o.toString()),a=x.substr(0,4),u=x.substr(4);n=n.substr(0,19)+a+"-"+u;var i,l=(i=(new Date).getTime()-new Date(2019,0,1).getTime(),Math.floor(i/864e5)),c=f(o.origin),h=[t.USERREPORT,l,c].join("."),s=f(n+h+e);return[r,s,n,h].join(".")}return{readOrCreateDsu:function(){var r;try{r=storage.getDataFromLocalStorage(n)}catch(n){return null}r||(r=p());try{storage.setDataInLocalStorage(n,r)}catch(n){return null}return r}}}(); + + data.dsu = dsuModule.readOrCreateDsu(); + } else { + data.dsu = ''; + options.withCredentials = false; + } + + const endpoints = getEndpointsGroups(bidRequests); + const serverRequests = endpoints.map(item => ({ + method: 'GET', + url: item.endpoint, + data: { + ...data, + bids: getBids(item.bids), + rnd: Math.random() + }, + options: options + })); + + return serverRequests; +} + +function interpretResponse(serverResponse) { + let bidResponses = serverResponse && serverResponse.body; + + if (!bidResponses || !bidResponses.length) { + return []; + } + + return bidResponses.map(x => ({ + requestId: x.bidId, + cpm: x.bidDetails.cpm, + width: x.bidDetails.width, + height: x.bidDetails.height, + creativeId: x.bidDetails.creativeId, + currency: x.bidDetails.currency || 'USD', + netRevenue: x.bidDetails.netRevenue, + dealId: x.bidDetails.dealId, + ad: injectPixels(x.bidDetails.ad, x.bidDetails.noticeUrls, x.bidDetails.impressionScripts), + ttl: x.bidDetails.ttl, + })); +} + +export const spec = { + code: CONSTANTS.BIDDER_CODE, + gvlid: CONSTANTS.GVLID, + isBidRequestValid: isBidRequestValid, + buildRequests: buildRequests, + interpretResponse: interpretResponse +} + +registerBidder(spec); diff --git a/modules/apstreamBidAdapter.md b/modules/apstreamBidAdapter.md new file mode 100644 index 00000000000..9f32de5ac64 --- /dev/null +++ b/modules/apstreamBidAdapter.md @@ -0,0 +1,85 @@ +# Overview + +``` +Module Name: AP Stream Bidder Adapter +Module Type: Bidder Adapter +Maintainer: tech@audienceproject.com +gdpr_supported: true +tcf2_supported: true +``` + +# Description + +Module that connects to AP Stream source + +# Inherit from prebid.js +``` + var adUnits = [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { // mandatory and should be only one + banner: { + sizes: [[920,180], [920, 130]] + } + }, + bids: [{ + bidder: 'apstream', + params: { + publisherId: STREAM_PIBLISHER_ID // mandatory + } + }] + } + ]; +``` + +# Explicit ad-unit code +``` + var website = null; + switch (location.hostname) { + case "site1.com": + website = "S1"; + break; + case "site2.com": + website = "S2"; + break; + } + + var adUnits = [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { // mandatory and should be only one + banner: { + sizes: [[920,180], [920, 130]] + } + }, + bids: [{ + bidder: 'apstream', + params: { + publisherId: STREAM_PIBLISHER_ID, // mandatory + code: website + '_Leaderboard' + } + }] + } + ]; +``` + +# Explicit ad-unit ID +``` + var adUnits = [ + { + code: '/19968336/header-bid-tag-1', + mediaTypes: { // mandatory and should be only one + banner: { + sizes: [[920,180], [920, 130]] + } + }, + bids: [{ + bidder: 'apstream', + params: { + publisherId: STREAM_PIBLISHER_ID, // mandatory + adunitId: 1234 + } + }] + } + ]; +``` diff --git a/test/spec/modules/apstreamBidAdapter_spec.js b/test/spec/modules/apstreamBidAdapter_spec.js new file mode 100644 index 00000000000..2443805f6b1 --- /dev/null +++ b/test/spec/modules/apstreamBidAdapter_spec.js @@ -0,0 +1,306 @@ +// jshint esversion: 6, es3: false, node: true +import {assert, expect} from 'chai'; +import {config} from 'src/config.js'; +import {spec} from 'modules/apstreamBidAdapter.js'; +import * as utils from 'src/utils.js'; + +const validBidRequests = [{ + bidId: 'bidId', + adUnitCode: '/id/site1/header-ad', + sizes: [[980, 120], [980, 180]], + mediaTypes: { + banner: { + sizes: [[980, 120], [980, 180]] + } + }, + params: { + publisherId: '1234' + } +}]; + +describe('AP Stream adapter', function() { + describe('isBidRequestValid', function() { + const bid = { + bidder: 'apstream', + mediaTypes: { + banner: { + sizes: [300, 250] + } + }, + params: { + } + }; + + it('should return true when publisherId is configured and one media type', function() { + bid.params.publisherId = '1234'; + assert(spec.isBidRequestValid(bid)) + }); + + it('should return false when publisherId is configured and two media types', function() { + bid.mediaTypes.video = {sizes: [300, 250]}; + assert.isFalse(spec.isBidRequestValid(bid)) + }); + }); + + describe('buildRequests', function() { + it('should send request with correct structure', function() { + const request = spec.buildRequests(validBidRequests, { })[0]; + + assert.equal(request.method, 'GET'); + assert.deepEqual(request.options, {withCredentials: false}); + assert.ok(request.data); + }); + + it('should send request with different endpoints', function() { + const validTwoBidRequests = [ + ...validBidRequests, + ...[{ + bidId: 'bidId2', + adUnitCode: '/id/site1/header-ad', + sizes: [[980, 980], [980, 900]], + mediaTypes: { + banner: { + sizes: [[980, 980], [980, 900]] + } + }, + params: { + publisherId: '1234', + endpoint: 'site2.com' + } + }] + ]; + + const request = spec.buildRequests(validTwoBidRequests, {}); + assert.isArray(request); + assert.lengthOf(request, 2); + assert.equal(request[1].data.bids, 'bidId2:t=b,s=980x980_980x900,c=/id/site1/header-ad'); + }); + + it('should send request with adUnit code', function() { + const adunitCodeValidBidRequests = [ + { + ...validBidRequests[0], + ...{ + params: { + code: 'Site1_Leaderboard' + } + } + } + ]; + + const request = spec.buildRequests(adunitCodeValidBidRequests, { })[0]; + assert.equal(request.data.bids, 'bidId:t=b,s=980x120_980x180,c=Site1_Leaderboard'); + }); + + it('should send request with adUnit id', function() { + const adunitIdValidBidRequests = [ + { + ...validBidRequests[0], + ...{ + params: { + adunitId: '12345' + } + } + } + ]; + + const request = spec.buildRequests(adunitIdValidBidRequests, { })[0]; + assert.equal(request.data.bids, 'bidId:t=b,s=980x120_980x180,u=12345'); + }); + + it('should send request with different media type', function() { + const types = { + 'audio': 'a', + 'banner': 'b', + 'native': 'n', + 'video': 'v' + } + Object.keys(types).forEach(key => { + const adunitIdValidBidRequests = [ + { + ...validBidRequests[0], + ...{ + mediaTypes: { + [key]: { + sizes: [300, 250] + } + } + } + } + ]; + + const request = spec.buildRequests(adunitIdValidBidRequests, { })[0]; + assert.equal(request.data.bids, `bidId:t=${types[key]},s=980x120_980x180,c=/id/site1/header-ad`); + }) + }); + + describe('gdpr', function() { + let mockConfig; + + beforeEach(function () { + mockConfig = { + consentManagement: { + cmpApi: 'iab' + } + }; + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should send GDPR Consent data', function() { + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '394': true + } + } + } + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + assert.equal(request.iab_consent, bidderRequest.gdprConsent.consentString); + }); + }); + + describe('dsu', function() { + it('should pass empty DSU if no consent', function() { + const request = spec.buildRequests(validBidRequests, {})[0].data; + + assert.isEmpty(request.dsu); + }); + + it('should pass DSU from local storage if set', function() { + let dsu = 'some_dsu'; + localStorage.setItem('apr_dsu', dsu); + + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '394': true + } + } + } + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + + assert.equal(request.dsu, dsu); + }); + + it('should generate new DSU if nothing in local storage', function() { + localStorage.removeItem('apr_dsu'); + + const bidderRequest = { + gdprConsent: { + gdprApplies: true, + consentString: 'consentDataString', + vendorData: { + vendorConsents: { + '394': true + } + } + } + }; + + const request = spec.buildRequests(validBidRequests, bidderRequest)[0].data; + let dsu = localStorage.getItem('apr_dsu'); + + assert.isNotEmpty(dsu); + assert.equal(request.dsu, dsu); + }); + }); + }); + + describe('interpretResponse', function () { + it('should return empty array if no body in response', function () { + const serverResponse = {}; + const bidRequest = {}; + + assert.isEmpty(spec.interpretResponse(serverResponse, bidRequest)); + }); + + it('should map server response', function () { + const serverResponse = { + body: [ + { + bidId: 123, + bidDetails: { + cpm: 1.23, + width: 980, + height: 300, + currency: 'DKK', + netRevenue: 'true', + creativeId: '1234', + dealId: '99008', + ad: '

Buy our something!

', + ttl: 360 + } + } + ] + }; + const bidRequest = {}; + + const response = spec.interpretResponse(serverResponse, bidRequest); + + const expected = { + requestId: 123, + cpm: 1.23, + width: 980, + height: 300, + currency: 'DKK', + creativeId: '1234', + netRevenue: 'true', + dealId: '99008', + ad: '

Buy our something!

', + ttl: 360 + }; + + assert.deepEqual(response[0], expected); + }); + + it('should add pixels to ad', function () { + const serverResponse = { + body: [ + { + bidId: 123, + bidDetails: { + cpm: 1.23, + width: 980, + height: 300, + currency: 'DKK', + creativeId: '1234', + netRevenue: 'true', + dealId: '99008', + ad: '

Buy our something!

', + ttl: 360, + noticeUrls: [ + 'site1', + 'site2' + ], + impressionScripts: [ + 'url_to_script' + ] + } + } + ] + }; + const bidRequest = {}; + + const response = spec.interpretResponse(serverResponse, bidRequest); + + assert.match(response[0].ad, /site1/); + assert.match(response[0].ad, /site2/); + }); + }); +}); From e9d670d354cdae6894f0fe4c76e1a88c1ccf008c Mon Sep 17 00:00:00 2001 From: Yevhenii Tykhostup Date: Fri, 24 Jul 2020 18:43:02 +0300 Subject: [PATCH 2/4] Swap minified DSU with transparent implementation --- modules/apstreamBidAdapter.js | 264 +++++++++++++++++++++++++++++++++- 1 file changed, 260 insertions(+), 4 deletions(-) diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index cc98565e2b5..3396c30bea9 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -10,6 +10,265 @@ const CONSTANTS = { }; const storage = getStorageManager(CONSTANTS.GVLID, CONSTANTS.BIDDER_CODE); +var dsuModule = (function() { + 'use strict'; + + var DSU_KEY = 'apr_dsu'; + var DSU_VERSION_NUMBER = '1'; + var SIGNATURE_SALT = 'YicAu6ZpNG'; + var DSU_CREATOR = {'USERREPORT': '1'}; + + function stringToU8(str) { + if (typeof TextEncoder === 'function') { + return new TextEncoder().encode(str); + } + str = unescape(encodeURIComponent(str)); + var bytes = new Uint8Array(str.length); + for (var i = 0, j = str.length; i < j; i++) { + bytes[i] = str.charCodeAt(i); + } + return bytes; + } + + function _add(a, b) { + var rl = a.l + b.l; + var a2 = { + h: a.h + b.h + (rl / 2 >>> 31) >>> 0, + l: rl >>> 0 + }; + a.h = a2.h; + a.l = a2.l; + } + function _xor(a, b) { + a.h ^= b.h; + a.h >>>= 0; + a.l ^= b.l; + a.l >>>= 0; + } + function _rotl(a, n) { + var a2 = { + h: a.h << n | a.l >>> (32 - n), + l: a.l << n | a.h >>> (32 - n) + }; + a.h = a2.h; + a.l = a2.l; + } + function _rotl32(a) { + var al = a.l; + a.l = a.h; + a.h = al; + } + + function _compress(v0, v1, v2, v3) { + _add(v0, v1); + _add(v2, v3); + _rotl(v1, 13); + _rotl(v3, 16); + _xor(v1, v0); + _xor(v3, v2); + _rotl32(v0); + _add(v2, v1); + _add(v0, v3); + _rotl(v1, 17); + _rotl(v3, 21); + _xor(v1, v2); + _xor(v3, v0); + _rotl32(v2); + } + function _getInt(a, offset) { + return a[offset + 3] << 24 | + a[offset + 2] << 16 | + a[offset + 1] << 8 | + a[offset]; + } + + function hash(key, m) { + if (typeof m === 'string') { + m = stringToU8(m); + } + var k0 = { + h: key[1] >>> 0, + l: key[0] >>> 0 + }; + var k1 = { + h: key[3] >>> 0, + l: key[2] >>> 0 + }; + var v0 = { + h: k0.h, + l: k0.l + }; + var v2 = k0; + var v1 = { + h: k1.h, + l: k1.l + }; + var v3 = k1; + var ml = m.length; + var ml7 = ml - 7; + var buf = new Uint8Array(new ArrayBuffer(8)); + _xor(v0, { + h: 0x736f6d65, + l: 0x70736575 + }); + _xor(v1, { + h: 0x646f7261, + l: 0x6e646f6d + }); + _xor(v2, { + h: 0x6c796765, + l: 0x6e657261 + }); + _xor(v3, { + h: 0x74656462, + l: 0x79746573 + }); + var mp = 0; + while (mp < ml7) { + var mi = { + h: _getInt(m, mp + 4), + l: _getInt(m, mp) + }; + _xor(v3, mi); + _compress(v0, v1, v2, v3); + _compress(v0, v1, v2, v3); + _xor(v0, mi); + mp += 8; + } + buf[7] = ml; + var ic = 0; + while (mp < ml) { + buf[ic++] = m[mp++]; + } + while (ic < 7) { + buf[ic++] = 0; + } + var mil = { + h: buf[7] << 24 | buf[6] << 16 | buf[5] << 8 | buf[4], + l: buf[3] << 24 | buf[2] << 16 | buf[1] << 8 | buf[0] + }; + _xor(v3, mil); + _compress(v0, v1, v2, v3); + _compress(v0, v1, v2, v3); + _xor(v0, mil); + _xor(v2, { + h: 0, + l: 0xff + }); + _compress(v0, v1, v2, v3); + _compress(v0, v1, v2, v3); + _compress(v0, v1, v2, v3); + _compress(v0, v1, v2, v3); + var h = v0; + _xor(h, v1); + _xor(h, v2); + _xor(h, v3); + return h; + } + + function hashHex(key, m) { + var r = hash(key, m); + return ('0000000' + r.h.toString(16)).substr(-8) + + ('0000000' + r.l.toString(16)).substr(-8); + } + + var SIPHASH_KEY = [0x86395a57, 0x6b5ba7f7, 0x69732c07, 0x2a6ef48d]; + var hashWithKey = hashHex.bind(null, SIPHASH_KEY); + + var parseUrlRegex = new RegExp('^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\\?([^#]*))?(#(.*))?'); + var overwrite = null; + var cache = {}; + function parseUrl(url) { + var addscheme = + url.indexOf('/') !== 0 && + url.indexOf('/') !== -1 && + (url.indexOf(':') === -1 || url.indexOf(':') > url.indexOf('/')); + + var match = parseUrlRegex.exec(addscheme ? 'noscheme://' + url : url); + var res = { + scheme: addscheme ? '' : match[2] || '', + host: match[4] || '', + hostname: match[4] ? match[4].split(':')[0] : '', + pathname: match[5] || '', + search: match[7] || '', + hash: match[9] || '', + toString: function () { + return url; + } + }; + + res.origin = res.scheme + '://' + res.host; + return res; + } + + function location() { + var url = overwrite || window.location.toString(); + url = url.replace(/\.demo\.audienceproject\.com\//, '/'); + + if (cache.url === url) { + return cache.parsed; + } + var parsed = parseUrl(url); + cache.url = url; + cache.parsed = parsed; + return parsed; + } + + function getDaysSinceApEpoch() { + var timeDiff = (new Date()).getTime() - (new Date(2019, 0, 1)).getTime(); + var daysSinceApEpoch = Math.floor(timeDiff / (1000 * 3600 * 24)); + return daysSinceApEpoch; + } + + function generateDsu() { + var dsuId = utils.generateUUID(); + var loc = location(); + + var dsuIdSuffix = hashWithKey(dsuId + loc.toString()); + var suffix4 = dsuIdSuffix.substr(0, 4); + var suffix8 = dsuIdSuffix.substr(4); + + dsuId = dsuId.substr(0, 19) + suffix4 + '-' + suffix8; + + var daysSinceApEpoch = getDaysSinceApEpoch(); + var originHash = hashWithKey(loc.origin); + + var metadata = [ + DSU_CREATOR.USERREPORT, + daysSinceApEpoch, + originHash + ].join('.'); + var signature = hashWithKey(dsuId + metadata + SIGNATURE_SALT); + + return [DSU_VERSION_NUMBER, signature, dsuId, metadata].join('.'); + } + + function readOrCreateDsu() { + var dsu; + try { + dsu = storage.getDataFromLocalStorage(DSU_KEY); + } catch (err) { + return null; + } + + if (!dsu) { + dsu = generateDsu(); + } + + try { + storage.setDataInLocalStorage(DSU_KEY, dsu); + } catch (err) { + return null; + } + + return dsu; + } + + return { + readOrCreateDsu: readOrCreateDsu + } +})(); + function serializeSizes(sizes) { if (Array.isArray(sizes[0]) === false) { sizes = [sizes]; @@ -156,7 +415,7 @@ function buildRequests(bidRequests, bidderRequest) { med: encodeURIComponent(window.location.href), auid: bidderRequest.auctionId, ref: document.referrer, - dnt: Number(window.navigator.doNotTrack) || 0, + dnt: utils.getDNT() ? 1 : 0, sr: getScreenParams() }; @@ -169,9 +428,6 @@ function buildRequests(bidRequests, bidderRequest) { const isConsent = getIabConsentString(bidderRequest); if (isConsent) { - // eslint-disable-next-line - const dsuModule=function(){"use strict";var n="apr_dsu",r="1",e="YicAu6ZpNG",t={USERREPORT:"1"};function o(n,r){var e=n.l+r.l,t={h:n.h+r.h+(e/2>>>31)>>>0,l:e>>>0};n.h=t.h,n.l=t.l}function x(n,r){n.h^=r.h,n.h>>>=0,n.l^=r.l,n.l>>>=0}function a(n,r){var e={h:n.h<>>32-r,l:n.l<>>32-r};n.h=e.h,n.l=e.l}function u(n){var r=n.l;n.l=n.h,n.h=r}function i(n,r,e,t){o(n,r),o(e,t),a(r,13),a(t,16),x(r,n),x(t,e),u(n),o(e,r),o(n,t),a(r,17),a(t,21),x(r,e),x(t,n),u(e)}function l(n,r){return n[r+3]<<24|n[r+2]<<16|n[r+1]<<8|n[r]}function c(n,r){"string"==typeof r&&(r=function(n){if("function"==typeof TextEncoder)return(new TextEncoder).encode(n);n=unescape(encodeURIComponent(n));for(var r=new Uint8Array(n.length),e=0,t=n.length;e>>0,l:n[0]>>>0},t={h:n[3]>>>0,l:n[2]>>>0},o={h:e.h,l:e.l},a=e,u={h:t.h,l:t.l},c=t,f=r.length,h=f-7,s=new Uint8Array(new ArrayBuffer(8));x(o,{h:1936682341,l:1886610805}),x(u,{h:1685025377,l:1852075885}),x(a,{h:1819895653,l:1852142177}),x(c,{h:1952801890,l:2037671283});for(var d=0;dn.indexOf("/")),e=h.exec(r?"noscheme://"+n:n),t={scheme:r?"":e[2]||"",host:e[4]||"",hostname:e[4]?e[4].split(":")[0]:"",pathname:e[5]||"",search:e[7]||"",hash:e[9]||"",toString:function(){return n}};return t.origin=t.scheme+"://"+t.host,t}(n);return d.url=n,d.parsed=r,r}function p(){var n=function(){if("function"==typeof Uint32Array&&"undefined"!=typeof crypto&&void 0!==crypto.getRandomValues){var n=new Uint32Array(4);crypto.getRandomValues(n);var r=-1;return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=n[++r>>3]>>r%8*4&15;return("x"===e?t:3&t|8).toString(16)})}var e=(new Date).getTime();return"undefined"!=typeof performance&&"function"==typeof performance.now&&(e+=performance.now()),"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(n){var r=(e+16*Math.random())%16|0;return e=Math.floor(e/16),("x"===n?r:3&r|8).toString(16)})}(),o=v(),x=f(n+o.toString()),a=x.substr(0,4),u=x.substr(4);n=n.substr(0,19)+a+"-"+u;var i,l=(i=(new Date).getTime()-new Date(2019,0,1).getTime(),Math.floor(i/864e5)),c=f(o.origin),h=[t.USERREPORT,l,c].join("."),s=f(n+h+e);return[r,s,n,h].join(".")}return{readOrCreateDsu:function(){var r;try{r=storage.getDataFromLocalStorage(n)}catch(n){return null}r||(r=p());try{storage.setDataInLocalStorage(n,r)}catch(n){return null}return r}}}(); - data.dsu = dsuModule.readOrCreateDsu(); } else { data.dsu = ''; From 9856b30a4c30bc36c27ddceaedcd18a53466d45f Mon Sep 17 00:00:00 2001 From: Yevhenii Tykhostup Date: Wed, 29 Jul 2020 11:36:34 +0300 Subject: [PATCH 3/4] Set DSU if consentManagement disabled --- modules/apstreamBidAdapter.js | 5 ++++- test/spec/modules/apstreamBidAdapter_spec.js | 6 ------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index 3396c30bea9..a9c2c214af5 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -307,7 +307,7 @@ function getIabConsentString(bidderRequest) { return getConsentStringFromPrebid(bidderRequest.gdprConsent); } - return null; + return 'disabled'; } function injectPixels(ad, pixels, scripts) { @@ -431,6 +431,9 @@ function buildRequests(bidRequests, bidderRequest) { data.dsu = dsuModule.readOrCreateDsu(); } else { data.dsu = ''; + } + + if (!isConsent || isConsent === 'disabled') { options.withCredentials = false; } diff --git a/test/spec/modules/apstreamBidAdapter_spec.js b/test/spec/modules/apstreamBidAdapter_spec.js index 2443805f6b1..82522a071d9 100644 --- a/test/spec/modules/apstreamBidAdapter_spec.js +++ b/test/spec/modules/apstreamBidAdapter_spec.js @@ -171,12 +171,6 @@ describe('AP Stream adapter', function() { }); describe('dsu', function() { - it('should pass empty DSU if no consent', function() { - const request = spec.buildRequests(validBidRequests, {})[0].data; - - assert.isEmpty(request.dsu); - }); - it('should pass DSU from local storage if set', function() { let dsu = 'some_dsu'; localStorage.setItem('apr_dsu', dsu); From 4ff3ed15d12bcef9743f4219d2a3296d9c8455b3 Mon Sep 17 00:00:00 2001 From: Yevhenii Tykhostup Date: Tue, 4 Aug 2020 08:43:50 +0300 Subject: [PATCH 4/4] Add possibility to disable DSU via config --- modules/apstreamBidAdapter.js | 9 ++++---- modules/apstreamBidAdapter.md | 12 ++++++++++ test/spec/modules/apstreamBidAdapter_spec.js | 24 ++++++++++++++++++++ 3 files changed, 41 insertions(+), 4 deletions(-) diff --git a/modules/apstreamBidAdapter.js b/modules/apstreamBidAdapter.js index a9c2c214af5..324c125f5ef 100644 --- a/modules/apstreamBidAdapter.js +++ b/modules/apstreamBidAdapter.js @@ -422,15 +422,16 @@ function buildRequests(bidRequests, bidderRequest) { const consentData = getRawConsentString(bidderRequest.gdprConsent); data.iab_consent = consentData; - let options = { + const options = { withCredentials: true }; const isConsent = getIabConsentString(bidderRequest); - if (isConsent) { - data.dsu = dsuModule.readOrCreateDsu(); - } else { + const noDsu = config.getConfig('apstream.noDsu'); + if (!isConsent || noDsu) { data.dsu = ''; + } else { + data.dsu = dsuModule.readOrCreateDsu(); } if (!isConsent || isConsent === 'disabled') { diff --git a/modules/apstreamBidAdapter.md b/modules/apstreamBidAdapter.md index 9f32de5ac64..e528307a003 100644 --- a/modules/apstreamBidAdapter.md +++ b/modules/apstreamBidAdapter.md @@ -83,3 +83,15 @@ Module that connects to AP Stream source } ]; ``` + +# DSU + +To disable DSU use config option: + +``` + pbjs.setConfig({ + apstream: { + noDsu: true + } + }); +``` diff --git a/test/spec/modules/apstreamBidAdapter_spec.js b/test/spec/modules/apstreamBidAdapter_spec.js index 82522a071d9..c6546a3bd83 100644 --- a/test/spec/modules/apstreamBidAdapter_spec.js +++ b/test/spec/modules/apstreamBidAdapter_spec.js @@ -216,6 +216,30 @@ describe('AP Stream adapter', function() { }); }); + describe('dsu config', function() { + let mockConfig; + beforeEach(function () { + mockConfig = { + apstream: { + noDsu: true + } + }; + sinon.stub(config, 'getConfig').callsFake((key) => { + return utils.deepAccess(mockConfig, key); + }); + }); + + afterEach(function () { + config.getConfig.restore(); + }); + + it('should not send DSU if it is disabled in config', function() { + const request = spec.buildRequests(validBidRequests, { })[0]; + + assert.equal(request.data.dsu, ''); + }); + }); + describe('interpretResponse', function () { it('should return empty array if no body in response', function () { const serverResponse = {};