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 all 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';
export 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);
154 changes: 146 additions & 8 deletions test/spec/modules/teadsBidAdapter_spec.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import {expect} from 'chai';
import {spec} from 'modules/teadsBidAdapter.js';
import {spec, storage} 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);
let cookiesAreEnabledStub, getCookieStub;

beforeEach(function () {
cookiesAreEnabledStub = sinon.stub(storage, 'cookiesAreEnabled');
getCookieStub = sinon.stub(storage, 'getCookie');
});

afterEach(function () {
cookiesAreEnabledStub.restore();
getCookieStub.restore();
});

describe('inherited functions', () => {
it('exists and is a function', () => {
Expand Down Expand Up @@ -102,7 +114,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 +286,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 +333,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 +387,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 +426,7 @@ describe('teadsBidAdapter', () => {
}
}
};
checkMediaTypesSizes(mediaTypesVideoSizes, '12x14')
checkMediaTypesSizes(mediaTypesVideoSizes, '12x14');
});

it('should use good mediaTypes banner sizes', function() {
Expand All @@ -427,7 +437,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 +451,135 @@ 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 () {
it('should not add firstPartyCookieTeadsId param to payload if cookies are not enabled', function () {
cookiesAreEnabledStub.returns(false);

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

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

it('should not add firstPartyCookieTeadsId param to payload if first-party cookie is not available', function () {
cookiesAreEnabledStub.returns(true);
getCookieStub.withArgs('_tfpvi').returns(undefined);

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 () {
cookiesAreEnabledStub.returns(true);
getCookieStub.withArgs('_tfpvi').returns('my-teads-id');

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