Skip to content

Commit

Permalink
CCPA modifications in the NextRoll adapter (prebid#5409)
Browse files Browse the repository at this point in the history
* Add native support

* Add response testing

* DRY test

* Change required from bool to int

* Set mediaType

* Fixes objects

* Fixes object access

* Remove ad property, only set it for banner

* Update tests

* Moving hardcoding values to constants

* Update docs with native information

* Revert "Add native support"

* Getting rid of CCPA adapter validation (#9)

* fix linter errors (#10)

Co-authored-by: Ricardo Azpeitia Pimentel <ricardo.azpeitia@nextroll.com>
Co-authored-by: Abimael Martinez <abijr@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 29, 2020
1 parent 240e605 commit 2a06f08
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 115 deletions.
144 changes: 68 additions & 76 deletions modules/nextrollBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ export const spec = {
*/
buildRequests: function (validBidRequests, bidderRequest) {
let topLocation = utils.parseUrl(utils.deepAccess(bidderRequest, 'refererInfo.referer'));
let consent = hasCCPAConsent(bidderRequest);
return validBidRequests.map((bidRequest, index) => {

return validBidRequests.map((bidRequest) => {
return {
method: 'POST',
options: {
withCredentials: consent,
withCredentials: true,
},
url: BIDDER_ENDPOINT,
data: {
Expand All @@ -59,9 +59,10 @@ export const spec = {
site: _getSite(bidRequest, topLocation),
seller: _getSeller(bidRequest),
device: _getDevice(bidRequest),
regs: _getRegs(bidderRequest)
}
}
})
};
});
},

/**
Expand All @@ -82,22 +83,22 @@ export const spec = {
}

function _getBanner(bidRequest) {
let sizes = _getSizes(bidRequest)
if (sizes === undefined) return undefined
return {format: sizes}
let sizes = _getSizes(bidRequest);
if (sizes === undefined) return undefined;
return {format: sizes};
}

function _getNative(mediaTypeNative) {
if (mediaTypeNative === undefined) return undefined
let assets = _getNativeAssets(mediaTypeNative)
if (assets === undefined || assets.length == 0) return undefined
if (mediaTypeNative === undefined) return undefined;
let assets = _getNativeAssets(mediaTypeNative);
if (assets === undefined || assets.length == 0) return undefined;
return {
request: {
native: {
assets: assets
}
}
}
};
}

/*
Expand All @@ -114,69 +115,71 @@ const NATIVE_ASSET_MAP = [
{id: 4, kind: 'img', key: 'logo', type: 2},
{id: 5, kind: 'data', key: 'sponsoredBy', type: 1},
{id: 6, kind: 'data', key: 'body', type: 2}
]
];

const ASSET_KIND_MAP = {
title: _getTitleAsset,
img: _getImageAsset,
data: _getDataAsset,
}
};

function _getAsset(mediaTypeNative, assetMap) {
let asset = mediaTypeNative[assetMap.key]
if (asset === undefined) return undefined
let assetFunc = ASSET_KIND_MAP[assetMap.kind]
const asset = mediaTypeNative[assetMap.key];
if (asset === undefined) return undefined;
const assetFunc = ASSET_KIND_MAP[assetMap.kind];
return {
id: assetMap.id,
required: (assetMap.required || !!asset.required) ? 1 : 0,
[assetMap.kind]: assetFunc(asset, assetMap)
}
};
}

function _getTitleAsset(title, _assetMap) {
return {len: title.len || 0}
return {len: title.len || 0};
}

function _getMinAspectRatio(aspectRatio, property) {
if (!utils.isPlainObject(aspectRatio)) return 1
if (!utils.isPlainObject(aspectRatio)) return 1;

let ratio = aspectRatio['ratio_' + property]
let min = aspectRatio['min_' + property]
const ratio = aspectRatio['ratio_' + property];
const min = aspectRatio['min_' + property];

if (utils.isNumber(ratio)) return ratio
if (utils.isNumber(min)) return min
if (utils.isNumber(ratio)) return ratio;
if (utils.isNumber(min)) return min;

return 1
return 1;
}

function _getImageAsset(image, assetMap) {
let sizes = image.sizes
let aspectRatio = image.aspect_ratios ? image.aspect_ratios[0] : undefined
const sizes = image.sizes;
const aspectRatio = image.aspect_ratios ? image.aspect_ratios[0] : undefined;

return {
type: assetMap.type,
w: (sizes ? sizes[0] : undefined),
h: (sizes ? sizes[1] : undefined),
wmin: _getMinAspectRatio(aspectRatio, 'width'),
hmin: _getMinAspectRatio(aspectRatio, 'height'),
}
};
}

function _getDataAsset(data, assetMap) {
return {
type: assetMap.type,
len: data.len || 0
}
};
}

function _getNativeAssets(mediaTypeNative) {
return NATIVE_ASSET_MAP.map(assetMap => _getAsset(mediaTypeNative, assetMap)).filter(asset => asset !== undefined)
return NATIVE_ASSET_MAP
.map(assetMap => _getAsset(mediaTypeNative, assetMap))
.filter(asset => asset !== undefined);
}

function _getUser(requests) {
let id = utils.deepAccess(requests, '0.userId.nextroll');
const id = utils.deepAccess(requests, '0.userId.nextroll');
if (id === undefined) {
return
return;
}

return {
Expand All @@ -186,7 +189,7 @@ function _getUser(requests) {
id
}]
}
}
};
}

function _buildResponse(bidResponse, bid) {
Expand All @@ -200,15 +203,15 @@ function _buildResponse(bidResponse, bid) {
currency: 'USD',
netRevenue: true,
ttl: 300
}
};
if (utils.isStr(bid.adm)) {
response.mediaType = BANNER
response.ad = utils.replaceAuctionPrice(bid.adm, bid.price)
response.mediaType = BANNER;
response.ad = utils.replaceAuctionPrice(bid.adm, bid.price);
} else {
response.mediaType = NATIVE
response.native = _getNativeResponse(bid.adm, bid.price)
response.mediaType = NATIVE;
response.native = _getNativeResponse(bid.adm, bid.price);
}
return response
return response;
}

const privacyLink = 'https://info.evidon.com/pub_info/573';
Expand All @@ -222,30 +225,30 @@ function _getNativeResponse(adm, price) {
impressionTrackers: adm.imptrackers.map(impTracker => utils.replaceAuctionPrice(impTracker, price)),
privacyLink: privacyLink,
privacyIcon: privacyIcon
}
};
return adm.assets.reduce((accResponse, asset) => {
let assetMaps = NATIVE_ASSET_MAP.filter(assetMap => assetMap.id === asset.id && asset[assetMap.kind] !== undefined)
if (assetMaps.length === 0) return accResponse
let assetMap = assetMaps[0]
accResponse[assetMap.key] = _getAssetResponse(asset, assetMap)
return accResponse
}, baseResponse)
const assetMaps = NATIVE_ASSET_MAP.filter(assetMap => assetMap.id === asset.id && asset[assetMap.kind] !== undefined);
if (assetMaps.length === 0) return accResponse;
const assetMap = assetMaps[0];
accResponse[assetMap.key] = _getAssetResponse(asset, assetMap);
return accResponse;
}, baseResponse);
}

function _getAssetResponse(asset, assetMap) {
switch (assetMap.kind) {
case 'title':
return asset.title.text
return asset.title.text;

case 'img':
return {
url: asset.img.url,
width: asset.img.w,
height: asset.img.h
}
};

case 'data':
return asset.data.value
return asset.data.value;
}
}

Expand All @@ -256,25 +259,25 @@ function _getSite(bidRequest, topLocation) {
publisher: {
id: utils.getBidIdParameter('publisherId', bidRequest.params)
}
}
};
}

function _getSeller(bidRequest) {
return {
id: utils.getBidIdParameter('sellerId', bidRequest.params)
}
};
}

function _getSizes(bidRequest) {
if (!utils.isArray(bidRequest.sizes)) {
return undefined
return undefined;
}
return bidRequest.sizes.filter(_isValidSize).map(size => {
return {
w: size[0],
h: size[1]
}
})
});
}

function _isValidSize(size) {
Expand All @@ -288,7 +291,18 @@ function _getDevice(_bidRequest) {
language: navigator['language'],
os: _getOs(navigator.userAgent.toLowerCase()),
osv: _getOsVersion(navigator.userAgent)
};
}

function _getRegs(bidderRequest) {
if (!bidderRequest || !bidderRequest.uspConsent) {
return undefined;
}
return {
ext: {
us_privacy: bidderRequest.uspConsent
}
};
}

function _getOs(userAgent) {
Expand All @@ -308,7 +322,7 @@ function _getOs(userAgent) {
}

function _getOsVersion(userAgent) {
let clientStrings = [
const clientStrings = [
{ s: 'Android', r: /Android/ },
{ s: 'iOS', r: /(iPhone|iPad|iPod)/ },
{ s: 'Mac OS X', r: /Mac OS X/ },
Expand All @@ -328,26 +342,4 @@ function _getOsVersion(userAgent) {
return cs ? cs.s : 'unknown';
}

export function hasCCPAConsent(bidderRequest) {
if (bidderRequest === undefined) return true;
if (typeof bidderRequest.uspConsent !== 'string') {
return true;
}
const usps = bidderRequest.uspConsent;
const version = usps[0];

// If we don't support the consent string, assume no-consent.
if (version !== '1' || usps.length < 3) {
return false;
}

const notice = usps[1];
const optOut = usps[2];

if (notice === 'N' || optOut === 'Y') {
return false;
}
return true;
}

registerBidder(spec);
46 changes: 7 additions & 39 deletions test/spec/modules/nextrollBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ describe('nextrollBidAdapter', function() {
expect(bannerObject.format[0].w).to.be.equal(300);
expect(bannerObject.format[0].h).to.be.equal(200);
});

it('sets the CCPA consent string', function () {
const us_privacy = '1YYY';
const request = spec.buildRequests([validBid], {'uspConsent': us_privacy})[0];

expect(request.data.regs.ext.us_privacy).to.be.equal(us_privacy);
});
});

describe('interpretResponse', function () {
Expand Down Expand Up @@ -258,43 +265,4 @@ describe('nextrollBidAdapter', function() {
expect(response[0].native).to.be.deep.equal(expectedResponse)
})
})

describe('hasCCPAConsent', function() {
function ccpaRequest(consentString) {
return {
bidderCode: 'bidderX',
auctionId: 'e3a336ad-2222-4a1c-bbbb-ecc7c5554a34',
uspConsent: consentString
};
}

const noNoticeCases = ['1NYY', '1NNN', '1N--'];
noNoticeCases.forEach((ccpaString, index) => {
it(`No notice should indicate no consent (case ${index})`, function () {
const req = ccpaRequest(ccpaString);
expect(hasCCPAConsent(req)).to.be.false;
});
});

const noConsentCases = ['1YYY', '1YYN', '1YY-'];
noConsentCases.forEach((ccpaString, index) => {
it(`Opt-Out should indicate no consent (case ${index})`, function () {
const req = ccpaRequest(ccpaString);
expect(hasCCPAConsent(req)).to.be.false;
});
});

const consentCases = [undefined, '1YNY', '1YN-', '1Y--', '1---'];
consentCases.forEach((ccpaString, index) => {
it(`should indicate consent (case ${index})`, function() {
const req = ccpaRequest(ccpaString);
expect(hasCCPAConsent(req)).to.be.true;
})
});

it('builds a request with no credentials', function () {
const noConsent = ccpaRequest('1YYY');
expect(spec.buildRequests([validBid], noConsent)[0].options.withCredentials).to.be.false;
});
});
});

0 comments on commit 2a06f08

Please sign in to comment.