Skip to content

Commit

Permalink
YieldlabBidAdapter
Browse files Browse the repository at this point in the history
- read dsa from bidderRequest
- put dsa response under meta.dsa not ext.dsa
- handle multiple transparency objects under new parameter dsatransparency
- only add query params if they are not undefined
  • Loading branch information
nkloeber committed Feb 2, 2024
1 parent 52d6870 commit 2a5187b
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 155 deletions.
88 changes: 52 additions & 36 deletions modules/yieldlabBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const spec = {
* @returns {boolean}
*/
isBidRequestValid(bid) {
return !!(bid && bid.params && bid.params.adslotId && bid.params.supplyId && isTransparencyCountOneOrLess(bid.params));
return !!(bid && bid.params && bid.params.adslotId && bid.params.supplyId);
},

/**
Expand Down Expand Up @@ -80,29 +80,6 @@ export const spec = {
if (floor) {
adslotFloors.push(bid.params.adslotId + ':' + floor);
}
const dsaRequired = bid.params.regs?.ext?.dsa?.required;
if (dsaRequired !== undefined) {
query.dsarequired = dsaRequired;
}
const dsaPubRender = bid.params.regs?.ext?.dsa?.pubrender;
if (dsaPubRender !== undefined) {
query.dsapubrender = dsaPubRender;
}
const dsaDataToPub = bid.params.regs?.ext?.dsa?.datatopub;
if (dsaDataToPub !== undefined) {
query.dsadatatopub = dsaDataToPub;
}
const dsaTransparency = bid.params.regs?.ext?.dsa?.transparency;
if (Array.isArray(dsaTransparency) && dsaTransparency.length > 0) {
const dsaDomain = dsaTransparency[0].domain;
if (dsaDomain !== undefined) {
query.dsadomain = dsaDomain;
}
const dsaParams = dsaTransparency[0].params;
if (Array.isArray(dsaParams) && dsaParams.length > 0) {
query.dsaparams = dsaParams.join(',');
}
}
});

if (bidderRequest) {
Expand All @@ -117,6 +94,33 @@ export const spec = {
query.consent = bidderRequest.gdprConsent.consentString;
}
}

if (bidderRequest.ortb2?.regs?.ext?.dsa !== undefined) {
const dsa = bidderRequest.ortb2.regs.ext.dsa;

assignIfNotUndefined(query, 'dsarequired', dsa.dsarequired);
assignIfNotUndefined(query, 'dsapubrender', dsa.pubrender);
assignIfNotUndefined(query, 'dsadatatopub', dsa.datatopub);

if (Array.isArray(dsa.transparency)) {
const filteredTransparencies = dsa.transparency.filter(({ domain, dsaparams }) => {
return domain && !domain.includes('~') && Array.isArray(dsaparams) && dsaparams.length > 0 && dsaparams.every(param => typeof param === 'number');
});

if (filteredTransparencies.length === 1) {
const { domain, dsaparams } = filteredTransparencies[0];
assignIfNotUndefined(query, 'dsadomain', domain);
assignIfNotUndefined(query, 'dsaparams', dsaparams.join(','));
} else if (filteredTransparencies.length > 1) {
const dsatransparency = filteredTransparencies.map(({ domain, dsaparams }) =>
`${domain}~${dsaparams.join('_')}`
).join('~~');
if (dsatransparency) {
query.dsatransparency = dsatransparency;
}
}
}
}
}

const adslots = adslotIds.join(',');
Expand Down Expand Up @@ -187,8 +191,7 @@ export const spec = {

const dsa = getDigitalServicesActObjectFromMatchedBid(matchedBid)
if (dsa !== undefined) {
bidResponse.ext = bidResponse.ext || {};
bidResponse.ext.dsa = dsa;
bidResponse.meta = { ...bidResponse.meta, dsa: dsa };
}

if (isVideo(bidRequest, adType)) {
Expand Down Expand Up @@ -563,23 +566,36 @@ function isImageAssetOfType(type) {
}

/**
* Determines if the 'transparency' array within the bid parameters has one or fewer elements.
* Retrieves the Digital Services Act (DSA) object from a matched bid.
* Only includes specific attributes (behalf, paid, transparency, adrender) from the DSA object.
*
* @param {Object} params - Bid parameters containing the nested 'transparency' array.
* @returns {boolean} - Returns true if the 'transparency' field is either undefined, or contains an array with at most one element; returns false otherwise.
* @param {Object} matchedBid - The server response body to inspect for the DSA information.
* @returns {Object|undefined} A copy of the DSA object if it exists, or undefined if not.
*/
function isTransparencyCountOneOrLess(params) {
return (params?.regs?.ext?.dsa?.transparency?.length ?? 0) <= 1;
function getDigitalServicesActObjectFromMatchedBid(matchedBid) {
if (matchedBid.dsa) {
const { behalf, paid, transparency, adrender } = matchedBid.dsa;
return {
...(behalf !== undefined && { behalf }),
...(paid !== undefined && { paid }),
...(transparency !== undefined && { transparency }),
...(adrender !== undefined && { adrender })
};
}
return undefined;
}

/**
* Retrieves the Digital Services Act (DSA) object from a matched bid.
* Conditionally assigns a value to a specified key on an object if the value is not undefined.
*
* @param {Object} matchedBid - The server response body to inspect for the DSA information.
* @returns {Object|undefined} A copy of the DSA object if it exists, or undefined if not.
* @param {Object} obj - The object to which the value will be assigned.
* @param {string} key - The key under which the value should be assigned.
* @param {*} value - The value to be assigned, if it is not undefined.
*/
function getDigitalServicesActObjectFromMatchedBid(matchedBid) {
return matchedBid.ext?.dsa ? { ...matchedBid.ext.dsa } : undefined;
function assignIfNotUndefined(obj, key, value) {
if (value !== undefined) {
obj[key] = value;
}
}

registerBidder(spec);
198 changes: 79 additions & 119 deletions test/spec/modules/yieldlabBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -161,51 +161,6 @@ const IAB_REQUEST = () => Object.assign(DEFAULT_REQUEST(), {
},
});

const DIGITAL_SERVICES_ACT_REQUEST = () => Object.assign(DEFAULT_REQUEST(), {
params: {
adslotId: '1111',
supplyId: '2222',
targeting: {
key1: 'value1',
key2: 'value2',
notDoubleEncoded: 'value3,value4',
},
customParams: {
extraParam: true,
foo: 'bar',
},
extId: 'abc',
iabContent: {
id: 'foo_id',
episode: '99',
title: 'foo_title,bar_title',
series: 'foo_series',
season: 's1',
artist: 'foo bar',
genre: 'baz',
isrc: 'CC-XXX-YY-NNNNN',
url: 'http://foo_url.de',
cat: ['cat1', 'cat2,ppp', 'cat3|||//'],
context: '7',
keywords: ['k1,', 'k2..'],
live: '0',
},
regs: {
ext: {
dsa: {
required: '1',
pubrender: '2',
datatopub: '3',
transparency: [{
domain: 'test.com',
params: [1, 2, 3]
}]
},
}
},
},
});

const RESPONSE = {
advertiser: 'yieldlab',
curl: 'https://www.yieldlab.de',
Expand Down Expand Up @@ -272,19 +227,35 @@ const PVID_RESPONSE = Object.assign({}, VIDEO_RESPONSE, {
});

const DIGITAL_SERVICES_ACT_RESPONSE = Object.assign({}, RESPONSE, {
ext: {
dsa: {
required: '1',
pubrender: '2',
datatopub: '3',
transparency: [{
domain: 'test.com',
params: [1, 2, 3]
}]
}
dsa: {
behalf: 'some-behalf',
paid: 'some-paid',
transparency: [{
domain: 'test.com',
dsaparams: [1, 2, 3]
}],
adrender: 1
}
});

const DIGITAL_SERVICES_ACT_CONFIG = {
ortb2: {
regs: {
ext: {
dsa: {
dsarequired: '1',
pubrender: '2',
datatopub: '3',
transparency: [{
domain: 'test.com',
dsaparams: [1, 2, 3]
}]
},
}
},
}
}

const REQPARAMS = {
json: true,
ts: 1234567890,
Expand Down Expand Up @@ -321,49 +292,6 @@ describe('yieldlabBidAdapter', () => {
it('should return false when required parameters are missing', () => {
expect(spec.isBidRequestValid({})).to.equal(false);
});

it('should return false if more than one transparency object is found', () => {
const request = {
params: {
adslotId: '1111',
supplyId: '2222',
regs: {
ext: {
dsa: {
transparency: [{
domain: 'test.com',
params: [1, 2, 3]
}, {
domain: 'yieldlab.com',
params: [1, 2, 3]
}]
},
}
},
},
};
expect(spec.isBidRequestValid(request)).to.equal(false);
});

it('should return true if one transparency object is found', () => {
const request = {
params: {
adslotId: '1111',
supplyId: '2222',
regs: {
ext: {
dsa: {
transparency: [{
domain: 'test.com',
params: [1, 2, 3]
}]
},
}
},
},
};
expect(spec.isBidRequestValid(request)).to.equal(true);
});
});

describe('buildRequests', () => {
Expand Down Expand Up @@ -590,40 +518,72 @@ describe('yieldlabBidAdapter', () => {
});

describe('Digital Services Act handling', () => {
it('does pass dsarequired parameter', () => {
const dsaRequest = DIGITAL_SERVICES_ACT_REQUEST();
beforeEach(() => {
config.setConfig(DIGITAL_SERVICES_ACT_CONFIG);
});

afterEach(() => {
config.resetConfig();
});

let request = spec.buildRequests([dsaRequest], REQPARAMS);
it('does pass dsarequired parameter', () => {
let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG });
expect(request.url).to.include('dsarequired=1');
});

it('does pass dsapubrender parameter', () => {
const dsaRequest = DIGITAL_SERVICES_ACT_REQUEST();

let request = spec.buildRequests([dsaRequest], REQPARAMS);
let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG });
expect(request.url).to.include('dsapubrender=2');
});

it('does pass dsadatatopub parameter', () => {
const dsaRequest = DIGITAL_SERVICES_ACT_REQUEST();

let request = spec.buildRequests([dsaRequest], REQPARAMS);
let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG });
expect(request.url).to.include('dsadatatopub=3');
});

it('does pass dsadomain parameter', () => {
const dsaRequest = DIGITAL_SERVICES_ACT_REQUEST();

let request = spec.buildRequests([dsaRequest], REQPARAMS);
let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG });
expect(request.url).to.include('dsadomain=test.com');
});

it('does pass encoded dsaparams parameter', () => {
const dsaRequest = DIGITAL_SERVICES_ACT_REQUEST();

let request = spec.buildRequests([dsaRequest], REQPARAMS);
let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG });
expect(request.url).to.include('dsaparams=1%2C2%2C3');
});

it('does pass multiple transparencies in dsatransparency param', () => {
const DSA_CONFIG_WITH_MULTIPLE_TRANSPARENCIES = {
ortb2: {
regs: {
ext: {
dsa: {
dsarequired: '1',
pubrender: '2',
datatopub: '3',
transparency: [
{
domain: 'test.com',
dsaparams: [1, 2, 3]
},
{
domain: 'example.com',
dsaparams: [4, 5, 6]
}
]
}
}
}
}
};

config.setConfig(DSA_CONFIG_WITH_MULTIPLE_TRANSPARENCIES);

let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DSA_CONFIG_WITH_MULTIPLE_TRANSPARENCIES });

expect(request.url).to.include('dsatransparency=test.com~1_2_3~~example.com~4_5_6');
expect(request.url).to.not.include('dsadomain');
expect(request.url).to.not.include('dsaparams');
});
});
});

Expand Down Expand Up @@ -817,14 +777,14 @@ describe('yieldlabBidAdapter', () => {
});

it('should get digital services act object in matched bid response', () => {
const result = spec.interpretResponse({body: [DIGITAL_SERVICES_ACT_RESPONSE]}, {validBidRequests: [DIGITAL_SERVICES_ACT_REQUEST()], queryParams: REQPARAMS});
const result = spec.interpretResponse({body: [DIGITAL_SERVICES_ACT_RESPONSE]}, {validBidRequests: [{...DEFAULT_REQUEST(), ...DIGITAL_SERVICES_ACT_CONFIG}], queryParams: REQPARAMS});

expect(result[0].requestId).to.equal('2d925f27f5079f');
expect(result[0].ext.dsa.required).to.equal('1');
expect(result[0].ext.dsa.pubrender).to.equal('2');
expect(result[0].ext.dsa.datatopub).to.equal('3');
expect(result[0].ext.dsa.transparency[0].domain).to.equal('test.com');
expect(result[0].ext.dsa.transparency[0].params).to.deep.equal([1, 2, 3]);
expect(result[0].meta.dsa.behalf).to.equal('some-behalf');
expect(result[0].meta.dsa.paid).to.equal('some-paid');
expect(result[0].meta.dsa.transparency[0].domain).to.equal('test.com');
expect(result[0].meta.dsa.transparency[0].dsaparams).to.deep.equal([1, 2, 3]);
expect(result[0].meta.dsa.adrender).to.equal(1);
});
});

Expand Down

0 comments on commit 2a5187b

Please sign in to comment.