From 665ffba9eacaabf927f1098b81a76faa0238497c Mon Sep 17 00:00:00 2001 From: Benoit Ruiz Date: Mon, 2 Aug 2021 19:31:02 +0200 Subject: [PATCH] Teads Bid Adapter: support floc and uid2 user IDs (#7116) * Teads adapter: support some user IDs * review changes * Stub access to storage functions in tests of Teads adapter Co-authored-by: Kylian Deau --- modules/teadsBidAdapter.js | 79 ++++++++--- test/spec/modules/teadsBidAdapter_spec.js | 154 ++++++++++++++++++++-- 2 files changed, 210 insertions(+), 23 deletions(-) diff --git a/modules/teadsBidAdapter.js b/modules/teadsBidAdapter.js index 32be7e62bbd..5e6062e5483 100644 --- a/modules/teadsBidAdapter.js +++ b/modules/teadsBidAdapter.js @@ -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'; @@ -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, @@ -41,6 +45,7 @@ export const spec = { */ buildRequests: function(validBidRequests, bidderRequest) { const bids = validBidRequests.map(buildRequestObject); + const payload = { referrer: getReferrerInfo(bidderRequest), pageReferrer: document.referrer, @@ -48,7 +53,10 @@ export const spec = { 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) { @@ -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, @@ -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 }; }, /** @@ -114,7 +122,7 @@ export const spec = { }); } return bidResponses; - }, + } }; function getReferrerInfo(bidderRequest) { @@ -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; } @@ -171,7 +183,7 @@ function isGlobalConsent(gdprData, apiVersion) { ? (gdprData.hasGlobalScope || gdprData.hasGlobalConsent) : gdprData && apiVersion === 2 ? !gdprData.isServiceSpecific - : false + : false; } function buildRequestObject(bid) { @@ -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; } @@ -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); diff --git a/test/spec/modules/teadsBidAdapter_spec.js b/test/spec/modules/teadsBidAdapter_spec.js index 3e1c3e78903..7fd3b70398b 100644 --- a/test/spec/modules/teadsBidAdapter_spec.js +++ b/test/spec/modules/teadsBidAdapter_spec.js @@ -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 = '"'; 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', () => { @@ -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); @@ -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', @@ -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', @@ -377,7 +387,7 @@ describe('teadsBidAdapter', () => { } } }; - checkMediaTypesSizes(mediaTypesPlayerSize, '32x34') + checkMediaTypesSizes(mediaTypesPlayerSize, '32x34'); }); it('should add schain info to payload if available', function () { @@ -416,7 +426,7 @@ describe('teadsBidAdapter', () => { } } }; - checkMediaTypesSizes(mediaTypesVideoSizes, '12x14') + checkMediaTypesSizes(mediaTypesVideoSizes, '12x14'); }); it('should use good mediaTypes banner sizes', function() { @@ -427,7 +437,7 @@ describe('teadsBidAdapter', () => { } } }; - checkMediaTypesSizes(mediaTypesBannerSize, '46x48') + checkMediaTypesSizes(mediaTypesBannerSize, '46x48'); }); it('should use good mediaTypes for both video and banner sizes', function() { @@ -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) {