Skip to content
Merged
Show file tree
Hide file tree
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 May 15, 2025
870f274
T Advertising Bid Adapter: add placementId
tb-emq May 21, 2025
62c090b
T Advertising Bid Adapter: add tradedesk id from usersync
tb-emq Jun 16, 2025
c25cce2
T Advertising Bid Adapter: handle prebid reporting and monitoring
tb-emq Jun 23, 2025
7792d4b
Merge remote-tracking branch 'github/master'
tb-emq Jun 24, 2025
4617bf1
T Advertising Bid Adapter: integrate bid floor module into adapter
tb-emq Jun 26, 2025
c429a09
T Advertising Bid Adapter: remove default bid floor
tb-emq Jun 30, 2025
2c4bbc5
T Advertising Bid Adapter: expanding adapter docs
tb-emq Jul 2, 2025
7b1d593
Merge branch 'prebid:master' into master
tb-emq Jul 2, 2025
a1b1b62
T Advertising Bid Adapter: add support for video ad unit
tb-emq Jul 4, 2025
53d89e3
Merge branch 'prebid:master' into master
tb-emq Jul 4, 2025
c78b35a
Merge remote-tracking branch 'github/master'
tb-emq Jul 4, 2025
3b7057d
T Advertising Bid Adapter: refactoring setting of placement id in bid…
tb-emq Jul 14, 2025
2e39ae5
Merge branch 'prebid:master' into master
tb-emq Jul 14, 2025
2c71f32
Merge branch 'prebid:master' into master
tb-emq Jul 15, 2025
bada91f
T Advertising Bid Adapter: add keepalive option to notification fallback
tb-emq Jul 15, 2025
604957a
Merge branch 'prebid:master' into master
tb-emq Jul 15, 2025
a66648a
Merge remote-tracking branch 'github/master'
tb-emq Jul 15, 2025
70e4c70
T Advertising Bid Adapter: fix indentation for linter
tb-emq Jul 15, 2025
a99d1ca
Merge branch 'prebid:master' into master
tb-emq Jul 22, 2025
e6e064c
T Advertising Bid Adapter: add support for ext.eid
tb-emq Jul 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
279 changes: 279 additions & 0 deletions modules/tadvertisingBidAdapter.js
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)) {
Comment thread
patmmccann marked this conversation as resolved.
// 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);
74 changes: 74 additions & 0 deletions modules/tadvertisingBidAdapter.md
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.
Loading
Loading