Skip to content

Commit

Permalink
Yieldlab Bid Adapter: Add Digital Services Act (DSA) handling (#10981)
Browse files Browse the repository at this point in the history
* YieldlabBidAdapter add Digital Services Act (DSA) handling for bid request and responses

* YieldlabBidAdapter
- 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 authored Feb 8, 2024
1 parent 6eaae9d commit e8426c7
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 4 deletions.
70 changes: 66 additions & 4 deletions modules/yieldlabBidAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ export const spec = {
* @returns {boolean}
*/
isBidRequestValid(bid) {
if (bid && bid.params && bid.params.adslotId && bid.params.supplyId) {
return true;
}
return false;
return !!(bid && bid.params && bid.params.adslotId && bid.params.supplyId);
},

/**
Expand Down Expand Up @@ -106,6 +103,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 @@ -174,6 +198,11 @@ export const spec = {
},
};

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

if (isVideo(bidRequest, adType)) {
const playersize = getPlayerSize(bidRequest);
if (playersize) {
Expand Down Expand Up @@ -545,4 +574,37 @@ function isImageAssetOfType(type) {
return asset => asset?.img?.type === type;
}

/**
* 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} 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 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;
}

/**
* Conditionally assigns a value to a specified key on an object if the value is not undefined.
*
* @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 assignIfNotUndefined(obj, key, value) {
if (value !== undefined) {
obj[key] = value;
}
}

registerBidder(spec);
110 changes: 110 additions & 0 deletions test/spec/modules/yieldlabBidAdapter_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,36 @@ const PVID_RESPONSE = Object.assign({}, VIDEO_RESPONSE, {
pvid: '43513f11-55a0-4a83-94e5-0ebc08f54a2c',
});

const DIGITAL_SERVICES_ACT_RESPONSE = Object.assign({}, RESPONSE, {
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 @@ -486,6 +516,75 @@ describe('yieldlabBidAdapter', () => {
expect(request.url).to.not.include('sizes');
});
});

describe('Digital Services Act handling', () => {
beforeEach(() => {
config.setConfig(DIGITAL_SERVICES_ACT_CONFIG);
});

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

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', () => {
let request = spec.buildRequests([DEFAULT_REQUEST()], { ...REQPARAMS, ...DIGITAL_SERVICES_ACT_CONFIG });
expect(request.url).to.include('dsapubrender=2');
});

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

it('does pass dsadomain parameter', () => {
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', () => {
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');
});
});
});

describe('interpretResponse', () => {
Expand Down Expand Up @@ -676,6 +775,17 @@ describe('yieldlabBidAdapter', () => {
const result = spec.interpretResponse({body: [VIDEO_RESPONSE]}, {validBidRequests: [VIDEO_REQUEST()], queryParams: REQPARAMS_IAB_CONTENT});
expect(result[0].vastUrl).to.include('&iab_content=id%3Afoo_id%2Cepisode%3A99%2Ctitle%3Afoo_title%252Cbar_title%2Cseries%3Afoo_series%2Cseason%3As1%2Cartist%3Afoo%2520bar%2Cgenre%3Abaz%2Cisrc%3ACC-XXX-YY-NNNNN%2Curl%3Ahttp%253A%252F%252Ffoo_url.de%2Ccat%3Acat1%7Ccat2%252Cppp%7Ccat3%257C%257C%257C%252F%252F%2Ccontext%3A7%2Ckeywords%3Ak1%252C%7Ck2..%2Clive%3A0');
});

it('should get digital services act object in matched bid response', () => {
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].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);
});
});

describe('getUserSyncs', () => {
Expand Down

0 comments on commit e8426c7

Please sign in to comment.