Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teads Bid Adapter: support floc and uid2 user IDs #7116

Merged
merged 3 commits into from
Aug 2, 2021
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions modules/teadsBidAdapter.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import {registerBidder} from '../src/adapters/bidderFactory.js';
const utils = require('../src/utils.js');
import {getStorageManager} from '../src/storageManager.js';
import * as utils from '../src/utils.js';

const BIDDER_CODE = 'teads';
const GVL_ID = 132;
const ENDPOINT_URL = 'https://a.teads.tv/hb/bid-request';
Expand All @@ -8,7 +10,9 @@ const gdprStatus = {
GDPR_APPLIES_GLOBAL: 11,
GDPR_DOESNT_APPLY: 0,
CMP_NOT_FOUND_OR_ERROR: 22
}
};
const FP_TEADS_ID_COOKIE_NAME = '_tfpvi';
const storage = getStorageManager(GVL_ID, BIDDER_CODE);

export const spec = {
code: BIDDER_CODE,
Expand Down Expand Up @@ -41,14 +45,18 @@ export const spec = {
*/
buildRequests: function(validBidRequests, bidderRequest) {
const bids = validBidRequests.map(buildRequestObject);

const payload = {
referrer: getReferrerInfo(bidderRequest),
pageReferrer: document.referrer,
networkBandwidth: getConnectionDownLink(window.navigator),
timeToFirstByte: getTimeToFirstByte(window),
data: bids,
deviceWidth: screen.width,
hb_version: '$prebid.version$'
hb_version: '$prebid.version$',
...getFLoCParameters(utils.deepAccess(validBidRequests, '0.userId.flocId')),
...getUnifiedId2Parameter(utils.deepAccess(validBidRequests, '0.userId.uid2')),
...getFirstPartyTeadsIdParameter()
};

if (validBidRequests[0].schain) {
Expand All @@ -57,11 +65,11 @@ export const spec = {

let gdpr = bidderRequest.gdprConsent;
if (bidderRequest && gdpr) {
let isCmp = (typeof gdpr.gdprApplies === 'boolean')
let isConsentString = (typeof gdpr.consentString === 'string')
let isCmp = typeof gdpr.gdprApplies === 'boolean';
let isConsentString = typeof gdpr.consentString === 'string';
let status = isCmp
? findGdprStatus(gdpr.gdprApplies, gdpr.vendorData, gdpr.apiVersion)
: gdprStatus.CMP_NOT_FOUND_OR_ERROR
: gdprStatus.CMP_NOT_FOUND_OR_ERROR;
payload.gdpr_iab = {
consent: isConsentString ? gdpr.consentString : '',
status: status,
Expand All @@ -70,14 +78,14 @@ export const spec = {
}

if (bidderRequest && bidderRequest.uspConsent) {
payload.us_privacy = bidderRequest.uspConsent
payload.us_privacy = bidderRequest.uspConsent;
}

const payloadString = JSON.stringify(payload);
return {
method: 'POST',
url: ENDPOINT_URL,
data: payloadString,
data: payloadString
};
},
/**
Expand Down Expand Up @@ -114,7 +122,7 @@ export const spec = {
});
}
return bidResponses;
},
}
};

function getReferrerInfo(bidderRequest) {
Expand Down Expand Up @@ -159,10 +167,14 @@ function getTimeToFirstByte(win) {
}

function findGdprStatus(gdprApplies, gdprData, apiVersion) {
let status = gdprStatus.GDPR_APPLIES_PUBLISHER
let status = gdprStatus.GDPR_APPLIES_PUBLISHER;
if (gdprApplies) {
if (isGlobalConsent(gdprData, apiVersion)) status = gdprStatus.GDPR_APPLIES_GLOBAL
} else status = gdprStatus.GDPR_DOESNT_APPLY
if (isGlobalConsent(gdprData, apiVersion)) {
status = gdprStatus.GDPR_APPLIES_GLOBAL;
}
} else {
status = gdprStatus.GDPR_DOESNT_APPLY;
}
return status;
}

Expand All @@ -171,7 +183,7 @@ function isGlobalConsent(gdprData, apiVersion) {
? (gdprData.hasGlobalScope || gdprData.hasGlobalConsent)
: gdprData && apiVersion === 2
? !gdprData.isServiceSpecific
: false
: false;
}

function buildRequestObject(bid) {
Expand Down Expand Up @@ -205,13 +217,15 @@ function concatSizes(bid) {
.reduce(function(acc, currSize) {
if (utils.isArray(currSize)) {
if (utils.isArray(currSize[0])) {
currSize.forEach(function (childSize) { acc.push(childSize) })
currSize.forEach(function (childSize) {
acc.push(childSize);
})
} else {
acc.push(currSize);
}
}
return acc;
}, [])
}, []);
} else {
return bid.sizes;
}
Expand All @@ -221,4 +235,39 @@ function _validateId(id) {
return (parseInt(id) > 0);
}

/**
* Get FLoC parameters to be sent in the bid request.
* @param `{id: string, version: string} | undefined` optionalFlocId FLoC user ID object available if "flocIdSystem" module is enabled.
* @returns `{} | {cohortId: string} | {cohortVersion: string} | {cohortId: string, cohortVersion: string}`
*/
function getFLoCParameters(optionalFlocId) {
if (!optionalFlocId) {
return {};
}
const cohortId = optionalFlocId.id ? { cohortId: optionalFlocId.id } : {};
const cohortVersion = optionalFlocId.version ? { cohortVersion: optionalFlocId.version } : {};
return { ...cohortId, ...cohortVersion };
}

/**
* Get unified ID v2 parameter to be sent in bid request.
* @param `{id: string} | undefined` optionalUid2 uid2 user ID object available if "uid2IdSystem" module is enabled.
* @returns `{} | {unifiedId2: string}`
*/
function getUnifiedId2Parameter(optionalUid2) {
return optionalUid2 ? { unifiedId2: optionalUid2.id } : {};
}

/**
* Get the first-party cookie Teads ID parameter to be sent in bid request.
* @returns `{} | {firstPartyCookieTeadsId: string}`
*/
function getFirstPartyTeadsIdParameter() {
if (!storage.cookiesAreEnabled()) {
return {};
}
const firstPartyTeadsId = storage.getCookie(FP_TEADS_ID_COOKIE_NAME);
return firstPartyTeadsId ? { firstPartyCookieTeadsId: firstPartyTeadsId } : {};
}

registerBidder(spec);
144 changes: 137 additions & 7 deletions test/spec/modules/teadsBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import {expect} from 'chai';
// import prebid.js to make processQueue available and flush hooks (in particular, those used for cookies)
import 'src/prebid.js';
import {spec} from 'modules/teadsBidAdapter.js';
import {newBidder} from 'src/adapters/bidderFactory.js';
import {getStorageManager} from 'src/storageManager';

const ENDPOINT = 'https://a.teads.tv/hb/bid-request';
const AD_SCRIPT = '<script type="text/javascript" class="teads" async="true" src="https://a.teads.tv/hb/getAdSettings"></script>"';

describe('teadsBidAdapter', () => {
const adapter = newBidder(spec);

before(function () {
// Following the introduction of tests involving reading/writing cookies,
// this allows for running this spec as a single file with:
// `gulp test --file "test/spec/modules/teadsBidAdapter_spec.js"`.
window.$$PREBID_GLOBAL$$.processQueue();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the stubbing of storage, you can get rid of this I believe.

This type of code in test files can cause downstream tests to be very flakey.

We do not want to modify the window object and its contents if we do not have to in tests.

});

describe('inherited functions', () => {
it('exists and is a function', () => {
expect(adapter.callBids).to.exist.and.to.be.a('function');
Expand Down Expand Up @@ -102,7 +112,7 @@ describe('teadsBidAdapter', () => {
'timeout': 3000
};

it('sends bid request to ENDPOINT via POST', function() {
it('should send bid request to ENDPOINT via POST', function() {
const request = spec.buildRequests(bidRequests, bidderResquestDefault);

expect(request.url).to.equal(ENDPOINT);
Expand Down Expand Up @@ -274,7 +284,6 @@ describe('teadsBidAdapter', () => {
});

it('should send GDPR to endpoint with 22 status', function() {
let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A==';
let bidderRequest = {
'auctionId': '1d1a030790a475',
'bidderRequestId': '22edbae2733bf6',
Expand Down Expand Up @@ -322,7 +331,6 @@ describe('teadsBidAdapter', () => {
});

it('should send GDPR to endpoint with 0 status when gdprApplies = false (vendorData = undefined)', function() {
let consentString = 'JRJ8RKfDeBNsERRDCSAAZ+A==';
let bidderRequest = {
'auctionId': '1d1a030790a475',
'bidderRequestId': '22edbae2733bf6',
Expand Down Expand Up @@ -377,7 +385,7 @@ describe('teadsBidAdapter', () => {
}
}
};
checkMediaTypesSizes(mediaTypesPlayerSize, '32x34')
checkMediaTypesSizes(mediaTypesPlayerSize, '32x34');
});

it('should add schain info to payload if available', function () {
Expand Down Expand Up @@ -416,7 +424,7 @@ describe('teadsBidAdapter', () => {
}
}
};
checkMediaTypesSizes(mediaTypesVideoSizes, '12x14')
checkMediaTypesSizes(mediaTypesVideoSizes, '12x14');
});

it('should use good mediaTypes banner sizes', function() {
Expand All @@ -427,7 +435,7 @@ describe('teadsBidAdapter', () => {
}
}
};
checkMediaTypesSizes(mediaTypesBannerSize, '46x48')
checkMediaTypesSizes(mediaTypesBannerSize, '46x48');
});

it('should use good mediaTypes for both video and banner sizes', function() {
Expand All @@ -441,7 +449,129 @@ describe('teadsBidAdapter', () => {
}
}
};
checkMediaTypesSizes(hybridMediaTypes, ['46x48', '50x34', '45x45'])
checkMediaTypesSizes(hybridMediaTypes, ['46x48', '50x34', '45x45']);
});

describe('User IDs', function () {
const baseBidRequest = {
'bidder': 'teads',
'params': {
'placementId': 10433394,
'pageId': 1234
},
'adUnitCode': 'adunit-code',
'sizes': [[300, 250], [300, 600]],
'bidId': '30b31c1838de1e',
'bidderRequestId': '22edbae2733bf6',
'auctionId': '1d1a030790a475',
'creativeId': 'er2ee',
'deviceWidth': 1680
};

describe('FLoC ID', function () {
it('should not add cohortId and cohortVersion params to payload if FLoC ID system is not enabled', function () {
const bidRequest = {
...baseBidRequest,
userId: {} // no "flocId" property -> assumption that the FLoC ID system is disabled
};

const request = spec.buildRequests([bidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload).not.to.have.property('cohortId');
expect(payload).not.to.have.property('cohortVersion');
});

it('should add cohortId param to payload if FLoC ID system is enabled and ID available, but not version', function () {
const bidRequest = {
...baseBidRequest,
userId: {
flocId: {
id: 'my-floc-id'
}
}
};

const request = spec.buildRequests([bidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload.cohortId).to.equal('my-floc-id');
expect(payload).not.to.have.property('cohortVersion');
});

it('should add cohortId and cohortVersion params to payload if FLoC ID system is enabled', function () {
const bidRequest = {
...baseBidRequest,
userId: {
flocId: {
id: 'my-floc-id',
version: 'chrome.1.1'
}
}
};

const request = spec.buildRequests([bidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload.cohortId).to.equal('my-floc-id');
expect(payload.cohortVersion).to.equal('chrome.1.1');
});
});

describe('Unified ID v2', function () {
it('should not add unifiedId2 param to payload if uid2 system is not enabled', function () {
const bidRequest = {
...baseBidRequest,
userId: {} // no "uid2" property -> assumption that the Unified ID v2 system is disabled
};

const request = spec.buildRequests([bidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload).not.to.have.property('unifiedId2');
});

it('should add unifiedId2 param to payload if uid2 system is enabled', function () {
const bidRequest = {
...baseBidRequest,
userId: {
uid2: {
id: 'my-unified-id-2'
}
}
};

const request = spec.buildRequests([bidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload.unifiedId2).to.equal('my-unified-id-2');
})
});

describe('First-party cookie Teads ID', function () {
const storage = getStorageManager(132, 'teads');

afterEach(function () {
// drop cookie
storage.setCookie('_tfpvi', '', new Date(0));
});

it('should not add firstPartyCookieTeadsId param to payload if first-party cookie is not available', function () {
const request = spec.buildRequests([baseBidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload).not.to.have.property('firstPartyCookieTeadsId');
});

it('should add firstPartyCookieTeadsId param to payload if first-party cookie is available', function () {
storage.setCookie('_tfpvi', 'my-teads-id');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do not want to actually be setting and reading from storage / cookies inside of tests.

We should be mocking the stubbing / mocking the storage manager, which has its own tests to verify it works.

Here is an example:

You'll need to export your storage var so that your tests can mock it:
https://github.com/prebid/Prebid.js/blob/master/modules/rubiconAnalyticsAdapter.js#L11

Then in your beforeEach you should setup the stub
https://github.com/prebid/Prebid.js/blob/master/test/spec/modules/rubiconAnalyticsAdapter_spec.js#L596-L600

Don't forget to restore it back in the afterEach
https://github.com/prebid/Prebid.js/blob/master/test/spec/modules/rubiconAnalyticsAdapter_spec.js#L629-L631

Then in your tests you can set them up to return what you want and make sure your module works as expected:
https://github.com/prebid/Prebid.js/blob/master/test/spec/modules/rubiconAnalyticsAdapter_spec.js#L1297


const request = spec.buildRequests([baseBidRequest], bidderResquestDefault);
const payload = JSON.parse(request.data);

expect(payload.firstPartyCookieTeadsId).to.equal('my-teads-id');
});
});
});

function checkMediaTypesSizes(mediaTypes, expectedSizes) {
Expand Down