From ede0c028b8b5124a120ce2284c51eb8369d69f7e Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Thu, 15 May 2025 08:22:57 +0200 Subject: [PATCH 01/12] T Advertising Bid Adapter: basic setup --- modules/tadvertisingBidAdapter.js | 84 ++++++++ modules/tadvertisingBidAdapter.md | 30 +++ .../modules/tadvertisingBidAdapter_spec.js | 197 ++++++++++++++++++ 3 files changed, 311 insertions(+) create mode 100644 modules/tadvertisingBidAdapter.js create mode 100644 modules/tadvertisingBidAdapter.md create mode 100644 test/spec/modules/tadvertisingBidAdapter_spec.js diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js new file mode 100644 index 00000000000..d915cd3bc17 --- /dev/null +++ b/modules/tadvertisingBidAdapter.js @@ -0,0 +1,84 @@ +import {deepAccess, isEmpty, deepSetValue, isStr, logWarn} from '../src/utils.js'; +import {registerBidder} from '../src/adapters/bidderFactory.js'; +import {BANNER, VIDEO} from "../src/mediaTypes.js"; +import {ortbConverter} from '../libraries/ortbConverter/converter.js'; + +const BIDDER_CODE = 'tadvertising'; +const GVL_ID = 213; +const ENDPOINT_URL = 'https://prebid.tads.xplosion.de/bid'; +const BID_TTL = 360; + +const MEDIA_TYPES = { + [BANNER]: 1, + [VIDEO]: 2, +}; + +const converter = ortbConverter({ + bidResponse: (buildBidResponse, bid, context) => { + let mediaType = BANNER; + if (bid.adm && bid.adm.startsWith(' 32) { + logWarn(BIDDER_CODE + ': params.publisherId must be 32 characters or less'); + return false; + } + return true; + }, + + + buildRequests: function (validBidRequests, bidderRequest) { + let data = converter.toORTB({validBidRequests, bidderRequest}) + deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId) + + if (isStr(deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { + data.user = data.user || {}; + data.user.buyeruid = bidderRequest.bids[0].userId.tdid; + } + + return { + method: 'POST', + url: ENDPOINT_URL, + data: data, + }; + }, + + + interpretResponse: function (response, serverRequest) { + if (isEmpty(response.body)) { + return []; + } + + deepSetValue(response, 'body.impid', deepAccess(serverRequest, 'data.imp.0.id')) // TODO: Remove before flight (launch) + deepSetValue(response, 'body.seatbid.0.bid.0.impid', deepAccess(serverRequest, 'data.imp.0.id')) // TODO: Remove before flight (launch) + + const bids = converter.fromORTB({response: response.body, request: serverRequest.data}).bids; + + bids.forEach(bid => { + bid.ttl = BID_TTL; + bid.netRevenue = true; + bid.currency = bid.currency || 'USD'; + bid.dealId = bid.dealId || null; + }) + + return bids; + } +} + +registerBidder(spec); diff --git a/modules/tadvertisingBidAdapter.md b/modules/tadvertisingBidAdapter.md new file mode 100644 index 00000000000..de987b476aa --- /dev/null +++ b/modules/tadvertisingBidAdapter.md @@ -0,0 +1,30 @@ +# Overview + +```markdown +Module Name: T-Advertising Bid Adapter +Module Type: Bidder Adapter +Maintainer: dev@emetriq.com + + +# Description + +Module that connects to T-Advertising Solutions demand sources. +Banner and Video ad formats are supported. + +# Test Parameters +``` + var adUnits = { + code: 'div-gpt-ad-1460505748561-0', + mediaTypes: { + banner: { + sizes: [[300, 250]], + } + }, + bids: [{ + bidder: 'tadvertising', + params: { + publisherId: 'your-publisher-id' + } + }] + }; +``` diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js new file mode 100644 index 00000000000..981a4035409 --- /dev/null +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -0,0 +1,197 @@ +import {expect} from 'chai'; +import {spec} from 'modules/tadvertisingBidAdapter'; + +describe('tadvertisingBidAdapter', () => { + function getBid() { + return { + 'bidder': 'tadvertising', + 'params': { + 'publisherId': '22222222', + }, + 'mediaTypes': { + 'banner': { + 'sizes': [ + [300, 250] + ] + } + }, + 'adUnitCode': 'adunit-code', + 'bidId': '30b31c1838de1e', + 'bidderRequestId': '22edbae2733bf6', + 'auctionId': '1d1a030790a475', + }; + } + + function getBidderRequest() { + return { + "bidderCode": "tadvertising", + "auctionId": "1d1a030790a475", + "bidderRequestId": "22edbae2733bf6", + "bids": [ + { + "bidder": "tadvertising", + "params": { + "publisherId": "22222222", + }, + "mediaTypes": { + "banner": { + "sizes": [ + [300, 250] + ] + } + }, + "adUnitCode": "adunit-code", + "bidId": "30b31c1838de1e", + "bidderRequestId": "22edbae2733bf6", + "auctionId": "1d1a030790a475" + } + ] + } + } + + describe('isBidRequestValid', function () { + it('should return true when required parameters are defined', function () { + expect(spec.isBidRequestValid(getBid())).to.equal(true); + }); + + it('should return false when publisherId not passed', function () { + let bid = getBid(); + delete bid.params.publisherId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when publisherId is longer than 32 characters', function () { + let bid = getBid(); + bid.params.publisherId = '111111111111111111111111111111111'; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + }); + + describe('buildRequests', function () { + function getConvertedBidRequest() { + return { + "imp": [ + { + "id": "30b31c1838de1e", + "banner": { + "topframe": 0, + "format": [ + { + "w": 300, + "h": 250 + } + ] + }, + "secure": 1 + } + ], + "id": "test_id", + "test": 0 + } + } + + it('should return a valid bid request', function () { + const request = spec.buildRequests(getBid(), getBidderRequest()); + const data = request.data; + const expected = getConvertedBidRequest() + + expect(request.method).to.equal('POST'); + expect(data.imp.id).to.equal(expected.imp.id); + expect(data.imp.banner).to.equal(expected.imp.banner); + }) + + it('should set user.buyeruid when userId.tdid is present', function () { + let bidderRequest = getBidderRequest(); + bidderRequest.bids[0].userId = {tdid: '1234567890'}; + const request = spec.buildRequests(getBid(), bidderRequest); + const data = request.data; + + expect(data.user.buyeruid).to.equal(bidderRequest.bids[0].userId.tdid); + }) + }); + + + describe('interpretResponse', function () { + function getBidderResponse() { + return { body: { + "id": "10b1e33f-fddc-4621-a472-d7bff0529cbf", + "cur": "USD", + "impid": "38c219964ca1998", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "38c219964ca1998", + "price": 0.78740156, + "adm": "

I am an ad

", + "cid": "ay35w7m", + "crid": "id8tke3f", + "adomain": [ + "emetriq.com" + ], + "cat": [ + "IAB2", + "IAB2-3" + ], + "h": 250, + "w": 300, + "mtype": 1 + } + ], + "seat": "2271" + } + ] + } + } + } + + it('should return an empty array when there is no body', function () { + const bidderRequest = getBidderRequest(); + const bidRequest = spec.buildRequests([], bidderRequest); + + const emptyArray = spec.interpretResponse({body: {}}, bidRequest); + + expect(emptyArray).to.deep.equal([]); + }) + + it('should return successful bid', function () { + const bidderRequest = getBidderRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const bidderResponse = getBidderResponse(); + const interpretedBids = spec.interpretResponse(bidderResponse, bidRequest); + const bid = interpretedBids[0]; + + expect(bid.mediaType).to.deep.equal("banner"); + expect(bid.ttl).to.equal(360); + expect(bid.netRevenue).to.equal(true); + expect(bid.currency).to.deep.equal("USD"); + expect(bid.dealId).to.equal(null); + }) + + it('should set currency to usd when response.body.curr is null', function () { + const bidderRequest = getBidderRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + const bidderResponse = getBidderResponse(); + bidderResponse.body.cur = null; + const interpretedBids = spec.interpretResponse(bidderResponse, bidRequest); + const bid = interpretedBids[0]; + + expect(bid.currency).to.deep.equal("USD"); + }) + + it('should set mediaType to video ', function () { + const bidderRequest = getBidderRequest(); + const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); + let bidderResponse = getBidderResponse(); + + bidderResponse.body.seatbid[0].bid[0].adm = 'testvast1'; + bidderResponse.body.seatbid[0].bid[0].mtype = 2; + + const interpretedBids = spec.interpretResponse(bidderResponse, bidRequest); + const bid = interpretedBids[0]; + + expect(bid.mediaType).to.deep.equal("video"); + }) + }); +}) From 870f2745648d0033c22fb7a7f070ace7f45a5fde Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Wed, 21 May 2025 07:23:57 +0200 Subject: [PATCH 02/12] T Advertising Bid Adapter: add placementId --- modules/tadvertisingBidAdapter.js | 5 +++++ modules/tadvertisingBidAdapter.md | 3 ++- test/spec/modules/tadvertisingBidAdapter_spec.js | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index d915cd3bc17..27469b103c6 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -39,6 +39,10 @@ export const spec = { logWarn(BIDDER_CODE + ': params.publisherId must be 32 characters or less'); return false; } + if (!bid.params.placementId) { + logWarn(BIDDER_CODE + ': Missing required parameter params.placementId'); + return false; + } return true; }, @@ -46,6 +50,7 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { let data = converter.toORTB({validBidRequests, bidderRequest}) deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId) + deepSetValue(data, 'imp.0.ext.gpid', bidderRequest.bids[0].params.placementId) if (isStr(deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { data.user = data.user || {}; diff --git a/modules/tadvertisingBidAdapter.md b/modules/tadvertisingBidAdapter.md index de987b476aa..f71b0c2b5d7 100644 --- a/modules/tadvertisingBidAdapter.md +++ b/modules/tadvertisingBidAdapter.md @@ -23,7 +23,8 @@ Banner and Video ad formats are supported. bids: [{ bidder: 'tadvertising', params: { - publisherId: 'your-publisher-id' + publisherId: 'your-publisher-id', + placementId: 'your-placement-id' } }] }; diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 981a4035409..6623507781f 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -7,6 +7,7 @@ describe('tadvertisingBidAdapter', () => { 'bidder': 'tadvertising', 'params': { 'publisherId': '22222222', + 'placementId': '33333333', }, 'mediaTypes': { 'banner': { @@ -32,6 +33,7 @@ describe('tadvertisingBidAdapter', () => { "bidder": "tadvertising", "params": { "publisherId": "22222222", + 'placementId': '33333333', }, "mediaTypes": { "banner": { @@ -60,6 +62,12 @@ describe('tadvertisingBidAdapter', () => { expect(spec.isBidRequestValid(bid)).to.equal(false); }); + it('should return false when placementId not passed', function () { + let bid = getBid(); + delete bid.params.placementId; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + it('should return false when publisherId is longer than 32 characters', function () { let bid = getBid(); bid.params.publisherId = '111111111111111111111111111111111'; From 62c090b4dfb7a164fad01c400c4539c7d44b8b82 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Mon, 16 Jun 2025 10:03:28 +0200 Subject: [PATCH 03/12] T Advertising Bid Adapter: add tradedesk id from usersync --- modules/tadvertisingBidAdapter.js | 26 +++++++- .../modules/tadvertisingBidAdapter_spec.js | 64 +++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index 27469b103c6..84fdba38c99 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -2,11 +2,13 @@ import {deepAccess, isEmpty, deepSetValue, isStr, logWarn} from '../src/utils.js import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from "../src/mediaTypes.js"; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; +import {hasPurpose1Consent} from '../src/utils/gdpr.js'; const BIDDER_CODE = 'tadvertising'; const GVL_ID = 213; const ENDPOINT_URL = 'https://prebid.tads.xplosion.de/bid'; const BID_TTL = 360; +const USER_SYNC_URL = 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=pxpinp0&ttd_tpi=1'; const MEDIA_TYPES = { [BANNER]: 1, @@ -83,7 +85,29 @@ export const spec = { }) return bids; - } + }, + + getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + const syncs = [] + + if (serverResponses[0]?.body?.ext?.uss === 1 && hasPurpose1Consent(gdprConsent)) { + var gdprParams; + if (typeof gdprConsent.gdprApplies === 'boolean') { + gdprParams = `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; + } else { + gdprParams = `&gdpr_consent=${gdprConsent.consentString}`; + } + + if (syncOptions.pixelEnabled) { + syncs.push({ + type: 'image', + url: USER_SYNC_URL + gdprParams + }); + } + } + + return syncs; + }, } registerBidder(spec); diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 6623507781f..d2d3f0f34ca 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -202,4 +202,68 @@ describe('tadvertisingBidAdapter', () => { expect(bid.mediaType).to.deep.equal("video"); }) }); + + describe('getUserSyncs', function() { + function getGdprConsent() { + return { + "vendorData": { + "gdprApplies": true, + "purpose": { + "consents": { + "1": true, + "2": true, + "3": true, + "4": true, + "5": true, + "6": true, + "7": true, + "8": true, + "9": true, + "10": true, + "11": true + }, + }, + "vendor": { + "consents": { + "21": true, + "213": true, + }, + }, + }, + "gdprApplies": true, + "apiVersion": 2 + } + } + + it('should return an empty array when sync is enabled but there are no bidResponses', function () { + let result = spec.getUserSyncs({ pixelEnabled: true }, [], getGdprConsent()) + + expect(result).to.have.length(0); + }); + + + it('should return an empty array with when sync is not enabled', function () { + let serverResponse = {body: {ext: { uss: 0}}}; + let result = spec.getUserSyncs({ pixelEnabled: true }, [serverResponse], getGdprConsent()) + + expect(result).to.have.length(0); + }); + + it('should return an empty array with when purpose one is not consented', function () { + let serverResponse = {body: {ext: { uss: 1}}}; + let consent = getGdprConsent() + consent.vendorData.purpose.consents[1] = false; + + let result = spec.getUserSyncs({ pixelEnabled: true }, [serverResponse], consent) + + expect(result).to.have.length(0); + }); + + it('should return an array with sync if purpose and venders are consented', function () { + let serverResponse = {body: {ext: { uss: 1}}}; + let result = spec.getUserSyncs({ pixelEnabled: true }, [serverResponse], getGdprConsent()) + + expect(result).to.have.length(1); + }); + }); }) From c25cce24e7aaf83cb216ecddc3db9375351dbc49 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Mon, 23 Jun 2025 11:45:25 +0200 Subject: [PATCH 04/12] T Advertising Bid Adapter: handle prebid reporting and monitoring - --- modules/tadvertisingBidAdapter.js | 140 +++++- .../modules/tadvertisingBidAdapter_spec.js | 397 +++++++++++++++++- 2 files changed, 525 insertions(+), 12 deletions(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index 84fdba38c99..a3b80f07ed2 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -1,20 +1,33 @@ -import {deepAccess, isEmpty, deepSetValue, isStr, logWarn} from '../src/utils.js'; +import { + deepAccess, + isEmpty, + deepSetValue, + isStr, + logWarn, + replaceAuctionPrice, + triggerPixel, + logError, +} from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from "../src/mediaTypes.js"; import {ortbConverter} from '../libraries/ortbConverter/converter.js'; import {hasPurpose1Consent} from '../src/utils/gdpr.js'; +import {ajax, sendBeacon} from "../src/ajax.js"; const BIDDER_CODE = 'tadvertising'; const GVL_ID = 213; const ENDPOINT_URL = 'https://prebid.tads.xplosion.de/bid'; -const BID_TTL = 360; +const NOTIFICATION_URL = 'https://prebid.tads.xplosion.de/notify'; const USER_SYNC_URL = 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=pxpinp0&ttd_tpi=1'; +const BID_TTL = 360; const MEDIA_TYPES = { [BANNER]: 1, [VIDEO]: 2, }; +const pageCache = {}; + const converter = ortbConverter({ bidResponse: (buildBidResponse, bid, context) => { let mediaType = BANNER; @@ -27,10 +40,83 @@ const converter = ortbConverter({ }, }); +export function buildSuccessNotification(bidEvent) { + return Object.fromEntries( + Object.entries({ + publisherId: deepAccess(bidEvent, 'params.0.publisherId'), + placementId: deepAccess(bidEvent, 'params.0.placementId'), + bidId: bidEvent.adId, + auctionId: bidEvent.auctionId, + adUnitCode: bidEvent.adUnitCode, + page: pageCache[bidEvent.requestId], + cpm: bidEvent.cpm, + currency: bidEvent.currency, + adId: bidEvent.adId, + creativeId: bidEvent.creativeId, + size: bidEvent.size, + dealId: bidEvent.dealId, + mediaType: bidEvent.mediaType, + status: bidEvent.status, + ttr: bidEvent.timeToRespond + }).filter(([_, value]) => value != null) + ); +} + +export function buildErrorNotification(bidEvent, error = null) { + return Object.fromEntries( + Object.entries({ + publisherId: deepAccess(bidEvent, 'bids.0.params.publisherId') || deepAccess(bidEvent, 'bids.0.params.0.publisherId'), + placementId: deepAccess(bidEvent, 'bids.0.params.placementId') || deepAccess(bidEvent, 'bids.0.params.0.placementId'), + bidId: deepAccess(bidEvent, 'bids.0.bidId'), + auctionId: deepAccess(bidEvent, 'auctionId'), + adUnitCode: deepAccess(bidEvent, 'bids.0.adUnitCode'), + page: deepAccess(bidEvent, 'refererInfo.page'), + timeout: bidEvent.timeout, + timedOut: error?.timedOut, + statusCode: error?.status, + response: error?.responseText + }).filter(([_, value]) => value != null) + ); +} + +export function buildTimeoutNotification(bidEvent) { + return Object.fromEntries( + Object.entries({ + publisherId: deepAccess(bidEvent, 'params.0.publisherId'), + placementId: deepAccess(bidEvent, 'params.0.placementId'), + bidId: deepAccess(bidEvent, 'bidId'), + auctionId: deepAccess(bidEvent, 'auctionId'), + adUnitCode: deepAccess(bidEvent, 'adUnitCode'), + page: deepAccess(bidEvent, 'ortb2.site.page'), + timeout: deepAccess(bidEvent, 'timeout'), + }).filter(([_, value]) => value != null) + ); +} + +export const sendNotification = (notifyUrl, eventType, data) => { + try { + const notificationUrl = `${notifyUrl}/${eventType}`; + const payload = JSON.stringify(data) + + if (!sendBeacon(notificationUrl, payload)) { + // Fallback to using AJAX if Beacon API is not supported + ajax(notificationUrl, null, payload, { + method: 'POST', + contentType: 'text/plain' + }); + } + } catch (error) { + logError(BIDDER_CODE, `Failed to notify event: ${eventType}`, error); + } +} + + export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, supportedMediaTypes: [BANNER, VIDEO], + sync_url: USER_SYNC_URL, + notify_url: NOTIFICATION_URL, isBidRequestValid: function (bid) { if (!bid.params.publisherId) { @@ -45,6 +131,10 @@ export const spec = { logWarn(BIDDER_CODE + ': Missing required parameter params.placementId'); return false; } + if (!bid.params.bidFloor) { + bid.params.bidFloor = 0; + logWarn(BIDDER_CODE + ': params.bidFloor is not set. Defaulting to 0'); + } return true; }, @@ -53,12 +143,16 @@ export const spec = { let data = converter.toORTB({validBidRequests, bidderRequest}) deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId) deepSetValue(data, 'imp.0.ext.gpid', bidderRequest.bids[0].params.placementId) + deepSetValue(data, 'imp.0.bidfloor', parseFloat(bidderRequest.bids[0].params.bidFloor)) + deepSetValue(data, 'imp.0.bidfloorcur', 'USD') if (isStr(deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { data.user = data.user || {}; data.user.buyeruid = bidderRequest.bids[0].userId.tdid; } - + bidderRequest.bids.forEach(bid => { + pageCache[bid.bidId] = deepAccess(bid, 'ortb2.site.page'); + }) return { method: 'POST', url: ENDPOINT_URL, @@ -71,9 +165,7 @@ export const spec = { if (isEmpty(response.body)) { return []; } - - deepSetValue(response, 'body.impid', deepAccess(serverRequest, 'data.imp.0.id')) // TODO: Remove before flight (launch) - deepSetValue(response, 'body.seatbid.0.bid.0.impid', deepAccess(serverRequest, 'data.imp.0.id')) // TODO: Remove before flight (launch) + deepSetValue(response, 'body.seatbid.0.bid.0.impid', deepAccess(serverRequest, 'data.imp.0.id')) const bids = converter.fromORTB({response: response.body, request: serverRequest.data}).bids; @@ -82,16 +174,20 @@ export const spec = { bid.netRevenue = true; bid.currency = bid.currency || 'USD'; bid.dealId = bid.dealId || null; + if (bid.vastXml) { + bid.vastXml = replaceAuctionPrice(bid.vastXml, bid.cpm); + } else { + bid.ad = replaceAuctionPrice(bid.ad, bid.cpm); + } }) return bids; }, - getUserSyncs: function(syncOptions, serverResponses, gdprConsent) { + getUserSyncs: function (syncOptions, serverResponses, gdprConsent) { const syncs = [] - - if (serverResponses[0]?.body?.ext?.uss === 1 && hasPurpose1Consent(gdprConsent)) { - var gdprParams; + if (serverResponses[0]?.body?.ext?.uss === 1 && gdprConsent && hasPurpose1Consent(gdprConsent)) { + let gdprParams; if (typeof gdprConsent.gdprApplies === 'boolean') { gdprParams = `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`; } else { @@ -105,9 +201,31 @@ export const spec = { }); } } - return syncs; }, + + onBidWon: function (bid) { + const payload = buildSuccessNotification(bid) + sendNotification(spec.notify_url, "won", payload) + }, + + onBidBillable: function (bid) { + if (bid.burl) { + triggerPixel(replaceAuctionPrice(bid.burl, bid.cpm)); + } + const payload = buildSuccessNotification(bid) + sendNotification(spec.notify_url, "billable", payload) + }, + + onTimeout: function (timeoutData) { + const payload = timeoutData.map(data => buildTimeoutNotification(data)) + sendNotification(spec.notify_url, 'timeout', payload) + }, + + onBidderError: function ({error, bidderRequest}) { + const payload = buildErrorNotification(bidderRequest, error) + sendNotification(spec.notify_url, 'error', payload) + } } registerBidder(spec); diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index d2d3f0f34ca..4365d9c9268 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -1,5 +1,8 @@ import {expect} from 'chai'; -import {spec} from 'modules/tadvertisingBidAdapter'; +import {spec, buildSuccessNotification, buildErrorNotification, buildTimeoutNotification, sendNotification} from 'modules/tadvertisingBidAdapter'; +import * as utils from '../../../src/utils.js'; +import * as ajax from '../../../src/ajax.js'; +import sinon from 'sinon'; describe('tadvertisingBidAdapter', () => { function getBid() { @@ -188,6 +191,7 @@ describe('tadvertisingBidAdapter', () => { expect(bid.currency).to.deep.equal("USD"); }) + /* it('should set mediaType to video ', function () { const bidderRequest = getBidderRequest(); const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -201,11 +205,13 @@ describe('tadvertisingBidAdapter', () => { expect(bid.mediaType).to.deep.equal("video"); }) + */ }); describe('getUserSyncs', function() { function getGdprConsent() { return { + "consentString": "CQTJuAAQTJuAAB7FlCENBvFsAP_gAEPgAAAALSNT_G__bWlr-T73aftkeYxP9_h77sQxBgbJE-4FzLvW_JwXx2E5NAzatqIKmRIAu3TBIQNlHJDURVCgaogVryDMaEyUoTNKJ6BkiBMRI2NYCFxvm4tjeQCY5vr991c1mB-t7dr83dzyy4hHn3a5_2S1WJCdAYetDfv8ZBKT-9IMd_x8v4v4_F7pE2-eS1n_pGvp6D9-YnM_9B299_bbffzPn__ql_-_X_vf_n37v943n77v___BaAAEw0KiCMsiAEIlAwggQAKCsICKBAEAACQNEBACYMCnIGAC6wkQAgBQADBACAAEGAAIAABIAEIgAoAKBAABAIFAAGABAMBAAwMAAYALAQCAAEB0DFMCCAQLABIzIoNMCUABIICWyoQSAIEFcIQizwCCBETBQAAAgAFAQAAPBYDEkgJWJBAFxBNAAAQAABRAgQIpGzAEFAZstBeDJ9GRpgGD5gmaUwDIAiCMjJNiE37TDxyFEKAA", "vendorData": { "gdprApplies": true, "purpose": { @@ -265,5 +271,394 @@ describe('tadvertisingBidAdapter', () => { expect(result).to.have.length(1); }); + + it('should return url with gdpr_consent string only', function () { + let serverResponse = {body: {ext: { uss: 1}}}; + let gdprConsent = getGdprConsent(); + gdprConsent.gdprApplies = null; + + let result = spec.getUserSyncs({ pixelEnabled: true }, [serverResponse], gdprConsent) + + expect(result).to.have.length(1); + expect(result[0].url).is.equal(spec.sync_url + '&gdpr_consent=CQTJuAAQTJuAAB7FlCENBvFsAP_gAEPgAAAALSNT_G__bWlr-T73aftkeYxP9_h77sQxBgbJE-4FzLvW_JwXx2E5NAzatqIKmRIAu3TBIQNlHJDURVCgaogVryDMaEyUoTNKJ6BkiBMRI2NYCFxvm4tjeQCY5vr991c1mB-t7dr83dzyy4hHn3a5_2S1WJCdAYetDfv8ZBKT-9IMd_x8v4v4_F7pE2-eS1n_pGvp6D9-YnM_9B299_bbffzPn__ql_-_X_vf_n37v943n77v___BaAAEw0KiCMsiAEIlAwggQAKCsICKBAEAACQNEBACYMCnIGAC6wkQAgBQADBACAAEGAAIAABIAEIgAoAKBAABAIFAAGABAMBAAwMAAYALAQCAAEB0DFMCCAQLABIzIoNMCUABIICWyoQSAIEFcIQizwCCBETBQAAAgAFAQAAPBYDEkgJWJBAFxBNAAAQAABRAgQIpGzAEFAZstBeDJ9GRpgGD5gmaUwDIAiCMjJNiE37TDxyFEKAA') + }) + + it('should return empty sync array when pixel is not enabled', function () { + let serverResponse = {body: {ext: { uss: 1}}}; + let gdprConsent = getGdprConsent(); + gdprConsent.gdprApplies = false; + + let result = spec.getUserSyncs({ pixelEnabled: false }, [serverResponse], gdprConsent) + + expect(result).is.empty; + }); + }); + + describe('buildSuccessNotification', function() { + it('should build correct BidResponseNotification', function() { + let bidderRequest = { + "params": [ + { + "publisherId": "publisher123", + "placementId": "placement456" + } + ], + "adId": "ad789", + "auctionId": "auction101112", + "adUnitCode": "adunit131415", + "requestId": "request161718", + "cpm": 1.25, + "currency": "USD", + "creativeId": "creative192021", + "size": "300x250", + "dealId": "deal222324", + "mediaType": "banner", + "status": "rendered", + "timeToRespond": 250 + } + let result = buildSuccessNotification(bidderRequest) + + expect(result).to.deep.equal({ + "adId": "ad789", + "adUnitCode": "adunit131415", + "auctionId": "auction101112", + "bidId": "ad789", + "cpm": 1.25, + "creativeId": "creative192021", + "currency": "USD", + "dealId": "deal222324", + "mediaType": "banner", + "placementId": "placement456", + "publisherId": "publisher123", + "size": "300x250", + "status": "rendered", + "ttr": 250 + }); + }); + }); + + + describe('buildErrorNotification', function() { + it('should build correct BidErrorResponseNotification', function() { + let bidderRequest = { + "bids": [ + { + "params": { + "publisherId": "publisher123", + "placementId": "placement456" + }, + "bidId": "bid789", + "adUnitCode": "adunit101112" + } + ], + "auctionId": "auction131415", + "refererInfo": { + "page": "https://example.com/page" + }, + "timeout": 3000 + } + + let error = { + "timedOut": false, + "status": 404, + "responseText": "Resource not found" + } + let result = buildErrorNotification(bidderRequest, error) + + expect(result).to.deep.equal({ + "publisherId": "publisher123", + "placementId": "placement456", + "bidId": "bid789", + "auctionId": "auction131415", + "adUnitCode": "adunit101112", + "page": "https://example.com/page", + "timeout": 3000, + "timedOut": false, + "statusCode": 404, + "response": "Resource not found" + }); + }); + + it('should build correct BidErrorResponseNotification with alternative structure', function() { + let bidderRequest = { + "bids": [ + { + "params": [{ + "publisherId": "publisher123", + "placementId": "placement456" + }], + "bidId": "bid789", + "adUnitCode": "adunit101112" + } + ], + "auctionId": "auction131415", + "refererInfo": { + "page": "https://example.com/page" + }, + "timeout": 3000 + } + + let error = { + "timedOut": false, + "status": 404, + "responseText": "Resource not found" + } + let result = buildErrorNotification(bidderRequest, error) + + expect(result).to.deep.equal({ + "publisherId": "publisher123", + "placementId": "placement456", + "bidId": "bid789", + "auctionId": "auction131415", + "adUnitCode": "adunit101112", + "page": "https://example.com/page", + "timeout": 3000, + "timedOut": false, + "statusCode": 404, + "response": "Resource not found" + }); + }); + + it('should build correctly when error is not present', function() { + let bidderRequest = { + "bids": [ + { + "params": [{ + "publisherId": "publisher123", + "placementId": "placement456" + }], + "bidId": "bid789", + "adUnitCode": "adunit101112" + } + ], + "auctionId": "auction131415", + "refererInfo": { + "page": "https://example.com/page" + }, + "timeout": 3000 + } + + let result = buildErrorNotification(bidderRequest) + + expect(result).to.deep.equal({ + "publisherId": "publisher123", + "placementId": "placement456", + "bidId": "bid789", + "auctionId": "auction131415", + "adUnitCode": "adunit101112", + "page": "https://example.com/page", + "timeout": 3000, + }); + }) + }); + + describe('buildTimeoutNotification', function() { + it('should build correct BidTimeoutNotification', function() { + let bid = { + "params": [ + { + "publisherId": "publisher123", + "placementId": "placement456" + } + ], + "bidId": "bid789", + "auctionId": "auction101112", + "adUnitCode": "adunit131415", + "ortb2": { + "site": { + "page": "https://example.com/page" + } + }, + "timeout": 3000 + } + let result = buildTimeoutNotification(bid) + + expect(result).to.deep.equal({ + "publisherId": "publisher123", + "placementId": "placement456", + "bidId": "bid789", + "auctionId": "auction101112", + "adUnitCode": "adunit131415", + "page": "https://example.com/page", + "timeout": 3000 + }); + }); + }); + + describe('sendNotification', function() { + let sendBeaconStub; + let ajaxStub; + let logErrorStub; + + beforeEach(function() { + spec.notify_url = 'https://test.com/notify'; + sendBeaconStub = sinon.stub(ajax, 'sendBeacon'); + ajaxStub = sinon.stub(ajax, 'ajax'); + logErrorStub = sinon.stub(utils, 'logError'); + }); + + afterEach(function() { + sendBeaconStub.restore(); + ajaxStub.restore(); + logErrorStub.restore(); + }); + + it('should send notification using sendBeacon when it is supported', function() { + const eventType = 'test'; + const data = { test: 'data' }; + sendBeaconStub.returns(true); + + sendNotification(spec.notify_url, eventType, data); + + expect(sendBeaconStub.calledOnce).to.be.true; + expect(sendBeaconStub.firstCall.args[0]).to.equal(spec.notify_url + '/test'); + expect(sendBeaconStub.firstCall.args[1]).to.equal(JSON.stringify(data)); + expect(ajaxStub.called).to.be.false; + }); + + it('should fallback to ajax when sendBeacon fails', function() { + const eventType = 'test'; + const data = { test: 'data' }; + sendBeaconStub.returns(false); + + sendNotification(spec.notify_url, eventType, data); + + expect(sendBeaconStub.calledOnce).to.be.true; + expect(ajaxStub.calledOnce).to.be.true; + expect(ajaxStub.firstCall.args[0]).to.equal(spec.notify_url + '/test'); + expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); + expect(ajaxStub.firstCall.args[3]).to.deep.equal({ + method: 'POST', + contentType: 'text/plain' + }); + }); + + it('should log error when an exception occurs', function() { + const eventType = 'test'; + const data = { test: 'data' }; + const error = new Error('Test error'); + sendBeaconStub.throws(error); + + sendNotification(spec.notify_url, eventType, data); + + expect(logErrorStub.calledOnce).to.be.true; + expect(logErrorStub.firstCall.args[0]).to.equal('tadvertising'); + expect(logErrorStub.firstCall.args[1]).to.equal('Failed to notify event: test'); + expect(logErrorStub.firstCall.args[2]).to.equal(error); + }); + }); + + describe('onBidWon', function() { + let sandbox; + let buildSuccessNotificationSpy; + + beforeEach(function() { + spec.notify_url = 'https://test.com/notify'; + sandbox = sinon.createSandbox(); + + // Create spies on the module functions + buildSuccessNotificationSpy = sandbox.spy(spec, 'onBidWon'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call onBidWon with correct parameters', function() { + const bid = { + adId: 'test-ad-id', + auctionId: 'test-auction-id', + cpm: 1.5 + }; + + spec.onBidWon(bid); + + expect(buildSuccessNotificationSpy.calledOnce).to.be.true; + expect(buildSuccessNotificationSpy.firstCall.args[0]).to.equal(bid); + }); + }); + + describe('onBidBillable', function() { + let sandbox; + let onBidBillableSpy; + + beforeEach(function() { + spec.notify_url = 'https://test.com/notify'; + sandbox = sinon.createSandbox(); + onBidBillableSpy = sandbox.spy(spec, 'onBidBillable'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call onBidBillable with correct parameters', function() { + const bid = { + adId: 'test-ad-id', + auctionId: 'test-auction-id', + cpm: 1.5, + burl: 'https://example.com/burl?price=${AUCTION_PRICE}' + }; + + spec.onBidBillable(bid); + + expect(onBidBillableSpy.calledOnce).to.be.true; + expect(onBidBillableSpy.firstCall.args[0]).to.equal(bid); + }); + }); + + describe('onTimeout', function() { + let sandbox; + let onTimeoutSpy; + + beforeEach(function() { + spec.notify_url = 'https://test.com/notify'; + sandbox = sinon.createSandbox(); + onTimeoutSpy = sandbox.spy(spec, 'onTimeout'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call onTimeout with correct parameters', function() { + const timeoutData = [ + { bidId: 'bid1', timeout: 1000 }, + { bidId: 'bid2', timeout: 2000 } + ]; + + spec.onTimeout(timeoutData); + + expect(onTimeoutSpy.calledOnce).to.be.true; + expect(onTimeoutSpy.firstCall.args[0]).to.equal(timeoutData); + }); + }); + + describe('onBidderError', function() { + let sandbox; + let onBidderErrorSpy; + + beforeEach(function() { + spec.notify_url = 'https://test.com/notify'; + sandbox = sinon.createSandbox(); + onBidderErrorSpy = sandbox.spy(spec, 'onBidderError'); + }); + + afterEach(function() { + sandbox.restore(); + }); + + it('should call onBidderError with correct parameters', function() { + const error = new Error('Test error'); + const bidderRequest = { + bidderCode: 'tadvertising', + bids: [{ bidId: 'test-bid-id' }] + }; + + spec.onBidderError({ error, bidderRequest }); + + expect(onBidderErrorSpy.calledOnce).to.be.true; + expect(onBidderErrorSpy.firstCall.args[0]).to.deep.equal({ error, bidderRequest }); + }); }); }) From 4617bf1e1c7cca9a7fc61685e9ce97fbb3ca96e9 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Thu, 26 Jun 2025 15:57:23 +0200 Subject: [PATCH 05/12] T Advertising Bid Adapter: integrate bid floor module into adapter - --- modules/tadvertisingBidAdapter.js | 42 ++++-- .../modules/tadvertisingBidAdapter_spec.js | 136 +++++++++++++++++- 2 files changed, 163 insertions(+), 15 deletions(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index a3b80f07ed2..bff85be5c98 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -7,6 +7,8 @@ import { replaceAuctionPrice, triggerPixel, logError, + isFn, + isPlainObject } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from "../src/mediaTypes.js"; @@ -93,6 +95,27 @@ export function buildTimeoutNotification(bidEvent) { ); } +export function getBidFloor (bid) { + // value from params takes precedance over value set by Floor Module + if (bid.params.bidfloor) { + return bid.params.bidfloor; + } + + if (!isFn(bid.getFloor)) { + return null; + } + + let floor = bid.getFloor({ + currency: 'USD', + mediaType: '*', + size: '*' + }); + if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') { + return floor.floor; + } + return null; +} + export const sendNotification = (notifyUrl, eventType, data) => { try { const notificationUrl = `${notifyUrl}/${eventType}`; @@ -110,7 +133,6 @@ export const sendNotification = (notifyUrl, eventType, data) => { } } - export const spec = { code: BIDDER_CODE, gvlid: GVL_ID, @@ -131,20 +153,23 @@ export const spec = { logWarn(BIDDER_CODE + ': Missing required parameter params.placementId'); return false; } - if (!bid.params.bidFloor) { - bid.params.bidFloor = 0; - logWarn(BIDDER_CODE + ': params.bidFloor is not set. Defaulting to 0'); - } return true; }, - buildRequests: function (validBidRequests, bidderRequest) { let data = converter.toORTB({validBidRequests, bidderRequest}) deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId) deepSetValue(data, 'imp.0.ext.gpid', bidderRequest.bids[0].params.placementId) - deepSetValue(data, 'imp.0.bidfloor', parseFloat(bidderRequest.bids[0].params.bidFloor)) - deepSetValue(data, 'imp.0.bidfloorcur', 'USD') + + const bidFloor = getBidFloor(bidderRequest.bids[0]) + if (bidFloor) { + deepSetValue(data, 'imp.0.bidfloor', bidFloor) + deepSetValue(data, 'imp.0.bidfloorcur', 'USD') + } else { + logWarn(BIDDER_CODE + ': params.bidFloor is not set. Defaulting to 0'); + deepSetValue(data, 'imp.0.bidfloor', 0) + deepSetValue(data, 'imp.0.bidfloorcur', 'USD') + } if (isStr(deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { data.user = data.user || {}; @@ -160,7 +185,6 @@ export const spec = { }; }, - interpretResponse: function (response, serverRequest) { if (isEmpty(response.body)) { return []; diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 4365d9c9268..9abedc3379d 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -1,5 +1,11 @@ import {expect} from 'chai'; -import {spec, buildSuccessNotification, buildErrorNotification, buildTimeoutNotification, sendNotification} from 'modules/tadvertisingBidAdapter'; +import {spec, + buildSuccessNotification, + buildErrorNotification, + buildTimeoutNotification, + sendNotification, + getBidFloor +} from 'modules/tadvertisingBidAdapter'; import * as utils from '../../../src/utils.js'; import * as ajax from '../../../src/ajax.js'; import sinon from 'sinon'; @@ -119,8 +125,45 @@ describe('tadvertisingBidAdapter', () => { expect(data.user.buyeruid).to.equal(bidderRequest.bids[0].userId.tdid); }) - }); + it('should set imp.0.bidfloor and imp.0.bidfloorcur when bidFloor is present', function () { + let bidderRequest = getBidderRequest(); + bidderRequest.bids[0].params.bidfloor = 1.5; + const request = spec.buildRequests(getBid(), bidderRequest); + const data = request.data; + + expect(data.imp[0].bidfloor).to.equal(1.5); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }) + + it('should set imp.0.bidfloor and imp.0.bidfloorcur when getFloor returns valid floor', function () { + let bidderRequest = getBidderRequest(); + bidderRequest.bids[0].getFloor = function() { + return { + floor: 2.5, + currency: 'USD' + }; + }; + const request = spec.buildRequests(getBid(), bidderRequest); + const data = request.data; + + expect(data.imp[0].bidfloor).to.equal(2.5); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }) + + it('should set imp.0.bidfloor to 0 and imp.0.bidfloorcur to USD when bidFloor is not present', function () { + let bidderRequest = getBidderRequest(); + // Ensure no bidfloor in params and no getFloor function + delete bidderRequest.bids[0].params.bidfloor; + delete bidderRequest.bids[0].getFloor; + + const request = spec.buildRequests(getBid(), bidderRequest); + const data = request.data; + + expect(data.imp[0].bidfloor).to.equal(0); + expect(data.imp[0].bidfloorcur).to.equal('USD'); + }) + }); describe('interpretResponse', function () { function getBidderResponse() { @@ -191,7 +234,6 @@ describe('tadvertisingBidAdapter', () => { expect(bid.currency).to.deep.equal("USD"); }) - /* it('should set mediaType to video ', function () { const bidderRequest = getBidderRequest(); const bidRequest = spec.buildRequests(bidderRequest.bids, bidderRequest); @@ -205,7 +247,6 @@ describe('tadvertisingBidAdapter', () => { expect(bid.mediaType).to.deep.equal("video"); }) - */ }); describe('getUserSyncs', function() { @@ -247,7 +288,6 @@ describe('tadvertisingBidAdapter', () => { expect(result).to.have.length(0); }); - it('should return an empty array with when sync is not enabled', function () { let serverResponse = {body: {ext: { uss: 0}}}; let result = spec.getUserSyncs({ pixelEnabled: true }, [serverResponse], getGdprConsent()) @@ -337,7 +377,6 @@ describe('tadvertisingBidAdapter', () => { }); }); - describe('buildErrorNotification', function() { it('should build correct BidErrorResponseNotification', function() { let bidderRequest = { @@ -661,4 +700,89 @@ describe('tadvertisingBidAdapter', () => { expect(onBidderErrorSpy.firstCall.args[0]).to.deep.equal({ error, bidderRequest }); }); }); + + describe('getBidFloor', function() { + it('should return bid.params.bidfloor when it exists', function() { + const bid = { + params: { + bidfloor: 0.5 + } + }; + + const result = getBidFloor(bid); + + expect(result).to.equal(0.5); + }); + + it('should return null when bid.getFloor is not a function', function() { + const bid = { + params: {} + }; + + const result = getBidFloor(bid); + + expect(result).to.be.null; + }); + + it('should return floor.floor when bid.getFloor returns valid floor object', function() { + const bid = { + params: {}, + getFloor: function() { + return { + floor: 1.0, + currency: 'USD' + }; + } + }; + + const result = getBidFloor(bid); + + expect(result).to.equal(1.0); + }); + + it('should return null when bid.getFloor returns object with non-USD currency', function() { + const bid = { + params: {}, + getFloor: function() { + return { + floor: 1.0, + currency: 'EUR' + }; + } + }; + + const result = getBidFloor(bid); + + expect(result).to.be.null; + }); + + it('should return null when bid.getFloor returns object with NaN floor', function() { + const bid = { + params: {}, + getFloor: function() { + return { + floor: NaN, + currency: 'USD' + }; + } + }; + + const result = getBidFloor(bid); + + expect(result).to.be.null; + }); + + it('should return null when bid.getFloor returns non-object', function() { + const bid = { + params: {}, + getFloor: function() { + return "not an object"; + } + }; + + const result = getBidFloor(bid); + + expect(result).to.be.null; + }); + }); }) From c429a098a80fd781ce797dc265c26d00d834ed47 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Mon, 30 Jun 2025 09:53:22 +0200 Subject: [PATCH 06/12] T Advertising Bid Adapter: remove default bid floor - --- modules/tadvertisingBidAdapter.js | 4 ---- test/spec/modules/tadvertisingBidAdapter_spec.js | 13 ------------- 2 files changed, 17 deletions(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index bff85be5c98..096ea8021e9 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -165,10 +165,6 @@ export const spec = { if (bidFloor) { deepSetValue(data, 'imp.0.bidfloor', bidFloor) deepSetValue(data, 'imp.0.bidfloorcur', 'USD') - } else { - logWarn(BIDDER_CODE + ': params.bidFloor is not set. Defaulting to 0'); - deepSetValue(data, 'imp.0.bidfloor', 0) - deepSetValue(data, 'imp.0.bidfloorcur', 'USD') } if (isStr(deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 9abedc3379d..507ddcc4bc5 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -150,19 +150,6 @@ describe('tadvertisingBidAdapter', () => { expect(data.imp[0].bidfloor).to.equal(2.5); expect(data.imp[0].bidfloorcur).to.equal('USD'); }) - - it('should set imp.0.bidfloor to 0 and imp.0.bidfloorcur to USD when bidFloor is not present', function () { - let bidderRequest = getBidderRequest(); - // Ensure no bidfloor in params and no getFloor function - delete bidderRequest.bids[0].params.bidfloor; - delete bidderRequest.bids[0].getFloor; - - const request = spec.buildRequests(getBid(), bidderRequest); - const data = request.data; - - expect(data.imp[0].bidfloor).to.equal(0); - expect(data.imp[0].bidfloorcur).to.equal('USD'); - }) }); describe('interpretResponse', function () { From 2c4bbc539f28152adbafd165819f06d25fe69a3a Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Wed, 2 Jul 2025 12:06:36 +0200 Subject: [PATCH 07/12] T Advertising Bid Adapter: expanding adapter docs - --- modules/tadvertisingBidAdapter.md | 74 ++++++++++++++++++++++++------- 1 file changed, 57 insertions(+), 17 deletions(-) diff --git a/modules/tadvertisingBidAdapter.md b/modules/tadvertisingBidAdapter.md index f71b0c2b5d7..98aab29e444 100644 --- a/modules/tadvertisingBidAdapter.md +++ b/modules/tadvertisingBidAdapter.md @@ -1,31 +1,71 @@ # Overview ```markdown -Module Name: T-Advertising Bid Adapter +Module Name: T-Advertising Solutions Bid Adapter Module Type: Bidder Adapter Maintainer: dev@emetriq.com - +``` # Description +The T-Advertising Solutions Bid Adapter is a module that connects to T-Advertising Solutions demand sources, enabling +publishers to access advertising demand. This adapter facilitates real-time bidding integration between Prebid.js and +T-Advertising Solutions' platform. -Module that connects to T-Advertising Solutions demand sources. -Banner and Video ad formats are supported. +This adapter supports both Banner and Video ad formats # Test Parameters -``` - var adUnits = { - code: 'div-gpt-ad-1460505748561-0', - mediaTypes: { - banner: { - sizes: [[300, 250]], - } - }, - bids: [{ +The following ad units demonstrate how to configure the adapter for different ad formats: + +## Banner Ad Unit Example +```javascript +var bannerAdUnit = { + code: 'myBannerAdUnit', + mediaTypes: { + banner: { + sizes: [400, 600], + } + }, + bids: [ + { bidder: 'tadvertising', params: { - publisherId: 'your-publisher-id', - placementId: 'your-placement-id' + publisherId: '1427ab10f2e448057ed3b422', + placementId: 'sidebar_1', + bidfloor: 0.95 // Optional - default is 0 + } + } + ] +}; +``` + +The banner ad unit configuration above demonstrates how to set up a basic banner implementation. + +## Video Ad Unit Example +```javascript +var videoAdUnit = { + code: 'myVideoAdUnit', + mediaTypes: { + video: { + mimes: ['video/mp4'], + minduration: 1, + maxduration: 60, + } + }, + bids: [ + { + bidder: "tadvertising", + params: { + publisherId: '1427ab10f2e448057ed3b422', + placementId: 'sidebar_1', + bidfloor: 0.95 // Optional - default is 0 } - }] - }; + } + ] +} ``` +The video ad unit configuration demonstrates how to set up a basic video implementation. + +# GDPR Compliance + +The T-Advertising Solutions adapter supports the IAB Europe Transparency & Consent Framework (TCF) for GDPR compliance. +When properly configured, the adapter will pass consent information to T-Advertising Solutions' servers. From a1b1b62aa717833a6840e38d7f91b68092ac39b7 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Fri, 4 Jul 2025 11:12:57 +0200 Subject: [PATCH 08/12] T Advertising Bid Adapter: add support for video ad unit - --- modules/tadvertisingBidAdapter.js | 30 +++- modules/tadvertisingBidAdapter.md | 3 + .../modules/tadvertisingBidAdapter_spec.js | 153 ++++++++++++++++++ 3 files changed, 185 insertions(+), 1 deletion(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index 096ea8021e9..a0ae0fde8fe 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -8,7 +8,8 @@ import { triggerPixel, logError, isFn, - isPlainObject + isPlainObject, + isInteger } from '../src/utils.js'; import {registerBidder} from '../src/adapters/bidderFactory.js'; import {BANNER, VIDEO} from "../src/mediaTypes.js"; @@ -153,6 +154,33 @@ export const spec = { logWarn(BIDDER_CODE + ': Missing required parameter params.placementId'); return false; } + + const mediaTypesBanner = deepAccess(bid, 'mediaTypes.banner'); + const mediaTypesVideo = deepAccess(bid, 'mediaTypes.video'); + + if (!mediaTypesBanner && !mediaTypesVideo) { + logWarn(BIDDER_CODE + ': one of mediaTypes.banner or mediaTypes.video must be passed'); + return false; + } + + if (FEATURES.VIDEO && mediaTypesVideo) { + if (!mediaTypesVideo.maxduration || !isInteger(mediaTypesVideo.maxduration)) { + logWarn(BIDDER_CODE + ': mediaTypes.video.maxduration must be set to the maximum video ad duration in seconds'); + return false; + } + if (!mediaTypesVideo.api || mediaTypesVideo.api.length === 0) { + logWarn(BIDDER_CODE + ': mediaTypes.video.api should be an array of supported api frameworks. See the Open RTB v2.5 spec for valid values'); + return false; + } + if (!mediaTypesVideo.mimes || mediaTypesVideo.mimes.length === 0) { + logWarn(BIDDER_CODE + ': mediaTypes.video.mimes should be an array of supported mime types'); + return false; + } + if (!mediaTypesVideo.protocols) { + logWarn(BIDDER_CODE + ': mediaTypes.video.protocols should be an array of supported protocols. See the Open RTB v2.5 spec for valid values'); + return false; + } + } return true; }, diff --git a/modules/tadvertisingBidAdapter.md b/modules/tadvertisingBidAdapter.md index 98aab29e444..b2d34f33119 100644 --- a/modules/tadvertisingBidAdapter.md +++ b/modules/tadvertisingBidAdapter.md @@ -49,6 +49,9 @@ var videoAdUnit = { mimes: ['video/mp4'], minduration: 1, maxduration: 60, + api: [1, 3], + placement: 3, + protocols: [2,3,5,6] } }, bids: [ diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 507ddcc4bc5..67624ee286e 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -61,6 +61,31 @@ describe('tadvertisingBidAdapter', () => { } describe('isBidRequestValid', function () { + // Helper function to check if FEATURES.VIDEO is enabled + function isVideoFeatureEnabled() { + // Create a test bid with video + let testBid = getBid(); + delete testBid.mediaTypes.banner; + testBid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3], + api: [1, 2], + maxduration: 30 + }; + + // Create the same bid but without maxduration + let testBidNoMaxduration = JSON.parse(JSON.stringify(testBid)); + delete testBidNoMaxduration.mediaTypes.video.maxduration; + + // If FEATURES.VIDEO is enabled, validation should fail without maxduration + // If not enabled, both should pass + return spec.isBidRequestValid(testBid) && !spec.isBidRequestValid(testBidNoMaxduration); + } + + const videoFeatureEnabled = isVideoFeatureEnabled(); + it('should return true when required parameters are defined', function () { expect(spec.isBidRequestValid(getBid())).to.equal(true); }); @@ -82,6 +107,134 @@ describe('tadvertisingBidAdapter', () => { bid.params.publisherId = '111111111111111111111111111111111'; expect(spec.isBidRequestValid(bid)).to.equal(false); }); + + it('should return false when neither mediaTypes.banner nor mediaTypes.video is present', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return true when mediaTypes.video is properly configured', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3], + api: [1, 2], + maxduration: 30 + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + + // Conditional tests based on FEATURES.VIDEO flag + if (videoFeatureEnabled) { + it('should return false when mediaTypes.video is missing maxduration (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3], + api: [1, 2] + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when mediaTypes.video.maxduration is not an integer (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3], + api: [1, 2], + maxduration: '30' + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when mediaTypes.video is missing api (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3], + maxduration: 30 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when mediaTypes.video.api is an empty array (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + protocols: [1, 2, 3], + api: [], + maxduration: 30 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when mediaTypes.video is missing mimes (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + protocols: [1, 2, 3], + api: [1, 2], + maxduration: 30 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when mediaTypes.video.mimes is an empty array (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: [], + protocols: [1, 2, 3], + api: [1, 2], + maxduration: 30 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + + it('should return false when mediaTypes.video is missing protocols (FEATURES.VIDEO enabled)', function () { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480], + mimes: ['video/mp4'], + api: [1, 2], + maxduration: 30 + }; + expect(spec.isBidRequestValid(bid)).to.equal(false); + }); + } else { + it('should skip video validation when FEATURES.VIDEO is not enabled', function() { + let bid = getBid(); + delete bid.mediaTypes.banner; + bid.mediaTypes.video = { + context: 'instream', + playerSize: [640, 480] + // Missing required fields, but should still pass if FEATURES.VIDEO is not enabled + }; + expect(spec.isBidRequestValid(bid)).to.equal(true); + }); + } }); describe('buildRequests', function () { From 3b7057dc8c74c2123271ecd72011478923aa2563 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Mon, 14 Jul 2025 10:12:44 +0200 Subject: [PATCH 09/12] T Advertising Bid Adapter: refactoring setting of placement id in bid impressions - --- modules/tadvertisingBidAdapter.js | 4 ++-- .../modules/tadvertisingBidAdapter_spec.js | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index a0ae0fde8fe..007168ded43 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -187,7 +187,6 @@ export const spec = { buildRequests: function (validBidRequests, bidderRequest) { let data = converter.toORTB({validBidRequests, bidderRequest}) deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId) - deepSetValue(data, 'imp.0.ext.gpid', bidderRequest.bids[0].params.placementId) const bidFloor = getBidFloor(bidderRequest.bids[0]) if (bidFloor) { @@ -199,8 +198,9 @@ export const spec = { data.user = data.user || {}; data.user.buyeruid = bidderRequest.bids[0].userId.tdid; } - bidderRequest.bids.forEach(bid => { + bidderRequest.bids.forEach((bid, index) => { pageCache[bid.bidId] = deepAccess(bid, 'ortb2.site.page'); + deepSetValue(data, `imp.${index}.ext.gpid`, bid.params.placementId); }) return { method: 'POST', diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 67624ee286e..8e79178707d 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -303,6 +303,25 @@ describe('tadvertisingBidAdapter', () => { expect(data.imp[0].bidfloor).to.equal(2.5); expect(data.imp[0].bidfloorcur).to.equal('USD'); }) + + it('should set placementId on every impression on bids', function() { + let bidderRequest = getBidderRequest(); + let bid1 = getBid() + bid1.bidId = '123' + bid1.params.placementId = '111' + + let bid2 = getBid() + bid2.bidId = '456' + bid2.params.placementId = '222' + + bidderRequest.bids = [bid1, bid2] + + const request = spec.buildRequests([bid1, bid2], bidderRequest); + const data = request.data; + + expect(data.imp[0].ext.gpid).to.equal(bidderRequest.bids[0].params.placementId); + expect(data.imp[1].ext.gpid).to.equal(bidderRequest.bids[1].params.placementId); + }) }); describe('interpretResponse', function () { From bada91f94e5a7758d608ac6b1fbc9f886e061f60 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Tue, 15 Jul 2025 15:19:09 +0200 Subject: [PATCH 10/12] T Advertising Bid Adapter: add keepalive option to notification fallback - --- modules/tadvertisingBidAdapter.js | 3 ++- test/spec/modules/tadvertisingBidAdapter_spec.js | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index 007168ded43..9353d1fa03e 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -126,7 +126,8 @@ export const sendNotification = (notifyUrl, eventType, data) => { // Fallback to using AJAX if Beacon API is not supported ajax(notificationUrl, null, payload, { method: 'POST', - contentType: 'text/plain' + contentType: 'text/plain', + keepalive: true, }); } } catch (error) { diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 8e79178707d..d1ef29c6717 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -727,7 +727,8 @@ describe('tadvertisingBidAdapter', () => { expect(ajaxStub.firstCall.args[2]).to.equal(JSON.stringify(data)); expect(ajaxStub.firstCall.args[3]).to.deep.equal({ method: 'POST', - contentType: 'text/plain' + contentType: 'text/plain', + keepalive: true, }); }); From 70e4c706afb4923a018b7697caa5d6d1ea903afb Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Tue, 15 Jul 2025 16:09:23 +0200 Subject: [PATCH 11/12] T Advertising Bid Adapter: fix indentation for linter - --- .../modules/tadvertisingBidAdapter_spec.js | 86 +++++++++---------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index d1ef29c6717..18421bb2363 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -327,35 +327,35 @@ describe('tadvertisingBidAdapter', () => { describe('interpretResponse', function () { function getBidderResponse() { return { body: { - "id": "10b1e33f-fddc-4621-a472-d7bff0529cbf", - "cur": "USD", - "impid": "38c219964ca1998", - "seatbid": [ - { - "bid": [ - { - "id": "1", - "impid": "38c219964ca1998", - "price": 0.78740156, - "adm": "

I am an ad

", - "cid": "ay35w7m", - "crid": "id8tke3f", - "adomain": [ - "emetriq.com" - ], - "cat": [ - "IAB2", - "IAB2-3" - ], - "h": 250, - "w": 300, - "mtype": 1 - } - ], - "seat": "2271" - } - ] - } + "id": "10b1e33f-fddc-4621-a472-d7bff0529cbf", + "cur": "USD", + "impid": "38c219964ca1998", + "seatbid": [ + { + "bid": [ + { + "id": "1", + "impid": "38c219964ca1998", + "price": 0.78740156, + "adm": "

I am an ad

", + "cid": "ay35w7m", + "crid": "id8tke3f", + "adomain": [ + "emetriq.com" + ], + "cat": [ + "IAB2", + "IAB2-3" + ], + "h": 250, + "w": 300, + "mtype": 1 + } + ], + "seat": "2271" + } + ] + } } } @@ -518,20 +518,20 @@ describe('tadvertisingBidAdapter', () => { let result = buildSuccessNotification(bidderRequest) expect(result).to.deep.equal({ - "adId": "ad789", - "adUnitCode": "adunit131415", - "auctionId": "auction101112", - "bidId": "ad789", - "cpm": 1.25, - "creativeId": "creative192021", - "currency": "USD", - "dealId": "deal222324", - "mediaType": "banner", - "placementId": "placement456", - "publisherId": "publisher123", - "size": "300x250", - "status": "rendered", - "ttr": 250 + "adId": "ad789", + "adUnitCode": "adunit131415", + "auctionId": "auction101112", + "bidId": "ad789", + "cpm": 1.25, + "creativeId": "creative192021", + "currency": "USD", + "dealId": "deal222324", + "mediaType": "banner", + "placementId": "placement456", + "publisherId": "publisher123", + "size": "300x250", + "status": "rendered", + "ttr": 250 }); }); }); From e6e064c5402030fb5cfde3a6b0715f127015cdb2 Mon Sep 17 00:00:00 2001 From: Tjorven Beckedorf Date: Tue, 22 Jul 2025 15:50:33 +0200 Subject: [PATCH 12/12] T Advertising Bid Adapter: add support for ext.eid - --- modules/tadvertisingBidAdapter.js | 7 ++-- .../modules/tadvertisingBidAdapter_spec.js | 35 ++++++++++++++----- 2 files changed, 29 insertions(+), 13 deletions(-) diff --git a/modules/tadvertisingBidAdapter.js b/modules/tadvertisingBidAdapter.js index 9353d1fa03e..83df42c467a 100644 --- a/modules/tadvertisingBidAdapter.js +++ b/modules/tadvertisingBidAdapter.js @@ -2,7 +2,6 @@ import { deepAccess, isEmpty, deepSetValue, - isStr, logWarn, replaceAuctionPrice, triggerPixel, @@ -195,10 +194,10 @@ export const spec = { deepSetValue(data, 'imp.0.bidfloorcur', 'USD') } - if (isStr(deepAccess(bidderRequest, 'bids.0.userId.tdid'))) { - data.user = data.user || {}; - data.user.buyeruid = bidderRequest.bids[0].userId.tdid; + if (deepAccess(validBidRequests[0], 'userIdAsEids')) { + deepSetValue(data, 'user.ext.eids', validBidRequests[0].userIdAsEids); } + bidderRequest.bids.forEach((bid, index) => { pageCache[bid.bidId] = deepAccess(bid, 'ortb2.site.page'); deepSetValue(data, `imp.${index}.ext.gpid`, bid.params.placementId); diff --git a/test/spec/modules/tadvertisingBidAdapter_spec.js b/test/spec/modules/tadvertisingBidAdapter_spec.js index 18421bb2363..95d53c8b5fc 100644 --- a/test/spec/modules/tadvertisingBidAdapter_spec.js +++ b/test/spec/modules/tadvertisingBidAdapter_spec.js @@ -270,15 +270,6 @@ describe('tadvertisingBidAdapter', () => { expect(data.imp.banner).to.equal(expected.imp.banner); }) - it('should set user.buyeruid when userId.tdid is present', function () { - let bidderRequest = getBidderRequest(); - bidderRequest.bids[0].userId = {tdid: '1234567890'}; - const request = spec.buildRequests(getBid(), bidderRequest); - const data = request.data; - - expect(data.user.buyeruid).to.equal(bidderRequest.bids[0].userId.tdid); - }) - it('should set imp.0.bidfloor and imp.0.bidfloorcur when bidFloor is present', function () { let bidderRequest = getBidderRequest(); bidderRequest.bids[0].params.bidfloor = 1.5; @@ -322,6 +313,32 @@ describe('tadvertisingBidAdapter', () => { expect(data.imp[0].ext.gpid).to.equal(bidderRequest.bids[0].params.placementId); expect(data.imp[1].ext.gpid).to.equal(bidderRequest.bids[1].params.placementId); }) + + it('should add unified ID info to user.ext.eids in the request', function () { + let bidderRequest = getBidderRequest(); + let bid1 = bidderRequest.bids[0] + bid1.userIdAsEids = [ + { + source: 'adserver.org', + uids: [ + { + atype: 1, + ext: { + rtiPartner: 'TDID' + }, + id: '00000000-0000-0000-0000-000000000000' + } + ] + } + ]; + + const expectedEids = bid1.userIdAsEids + + const request = spec.buildRequests(bidderRequest.bids, bidderRequest); + const data = request.data; + + expect(data.user.ext.eids).to.deep.equal(expectedEids) + }) }); describe('interpretResponse', function () {