-
Notifications
You must be signed in to change notification settings - Fork 2.4k
T Advertising Solutions Bid Adapter: initial release #13526
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
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
ede0c02
T Advertising Bid Adapter: basic setup
tb-emq 870f274
T Advertising Bid Adapter: add placementId
tb-emq 62c090b
T Advertising Bid Adapter: add tradedesk id from usersync
tb-emq c25cce2
T Advertising Bid Adapter: handle prebid reporting and monitoring
tb-emq 7792d4b
Merge remote-tracking branch 'github/master'
tb-emq 4617bf1
T Advertising Bid Adapter: integrate bid floor module into adapter
tb-emq c429a09
T Advertising Bid Adapter: remove default bid floor
tb-emq 2c4bbc5
T Advertising Bid Adapter: expanding adapter docs
tb-emq 7b1d593
Merge branch 'prebid:master' into master
tb-emq a1b1b62
T Advertising Bid Adapter: add support for video ad unit
tb-emq 53d89e3
Merge branch 'prebid:master' into master
tb-emq c78b35a
Merge remote-tracking branch 'github/master'
tb-emq 3b7057d
T Advertising Bid Adapter: refactoring setting of placement id in bid…
tb-emq 2e39ae5
Merge branch 'prebid:master' into master
tb-emq 2c71f32
Merge branch 'prebid:master' into master
tb-emq bada91f
T Advertising Bid Adapter: add keepalive option to notification fallback
tb-emq 604957a
Merge branch 'prebid:master' into master
tb-emq a66648a
Merge remote-tracking branch 'github/master'
tb-emq 70e4c70
T Advertising Bid Adapter: fix indentation for linter
tb-emq a99d1ca
Merge branch 'prebid:master' into master
tb-emq e6e064c
T Advertising Bid Adapter: add support for ext.eid
tb-emq File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,279 @@ | ||
| import { | ||
| deepAccess, | ||
| isEmpty, | ||
| deepSetValue, | ||
| logWarn, | ||
| replaceAuctionPrice, | ||
| triggerPixel, | ||
| logError, | ||
| isFn, | ||
| isPlainObject, | ||
| isInteger | ||
| } 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 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; | ||
| if (bid.adm && bid.adm.startsWith('<VAST')) { | ||
| mediaType = VIDEO; | ||
| } | ||
| bid.mtype = MEDIA_TYPES[mediaType]; | ||
|
|
||
| return buildBidResponse(bid, context); | ||
| }, | ||
| }); | ||
|
|
||
| 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 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}`; | ||
| 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', | ||
| keepalive: true, | ||
| }); | ||
| } | ||
| } 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) { | ||
| logWarn(BIDDER_CODE + ': Missing required parameter params.publisherId'); | ||
| return false; | ||
| } | ||
| if (bid.params.publisherId.length > 32) { | ||
| 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; | ||
| } | ||
|
|
||
| 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; | ||
| }, | ||
|
|
||
| buildRequests: function (validBidRequests, bidderRequest) { | ||
| let data = converter.toORTB({validBidRequests, bidderRequest}) | ||
| deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId) | ||
|
|
||
| const bidFloor = getBidFloor(bidderRequest.bids[0]) | ||
| if (bidFloor) { | ||
| deepSetValue(data, 'imp.0.bidfloor', bidFloor) | ||
| deepSetValue(data, 'imp.0.bidfloorcur', 'USD') | ||
| } | ||
|
|
||
| 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); | ||
| }) | ||
| return { | ||
| method: 'POST', | ||
| url: ENDPOINT_URL, | ||
| data: data, | ||
| }; | ||
| }, | ||
|
|
||
| interpretResponse: function (response, serverRequest) { | ||
| if (isEmpty(response.body)) { | ||
| return []; | ||
| } | ||
| 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; | ||
|
|
||
| bids.forEach(bid => { | ||
| bid.ttl = BID_TTL; | ||
| 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) { | ||
| const syncs = [] | ||
| 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 { | ||
| gdprParams = `&gdpr_consent=${gdprConsent.consentString}`; | ||
| } | ||
|
|
||
| if (syncOptions.pixelEnabled) { | ||
| syncs.push({ | ||
| type: 'image', | ||
| url: USER_SYNC_URL + gdprParams | ||
| }); | ||
| } | ||
| } | ||
| 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); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| # Overview | ||
|
|
||
| ```markdown | ||
| 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. | ||
|
|
||
| This adapter supports both Banner and Video ad formats | ||
|
|
||
| # Test Parameters | ||
| 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: '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, | ||
| api: [1, 3], | ||
| placement: 3, | ||
| protocols: [2,3,5,6] | ||
| } | ||
| }, | ||
| 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. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.