Skip to content

Commit da680b2

Browse files
authored
T Advertising Solutions Bid Adapter: initial release (prebid#13526)
* T Advertising Bid Adapter: basic setup * T Advertising Bid Adapter: add placementId * T Advertising Bid Adapter: add tradedesk id from usersync * T Advertising Bid Adapter: handle prebid reporting and monitoring - * T Advertising Bid Adapter: integrate bid floor module into adapter - * T Advertising Bid Adapter: remove default bid floor - * T Advertising Bid Adapter: expanding adapter docs - * T Advertising Bid Adapter: add support for video ad unit - * T Advertising Bid Adapter: refactoring setting of placement id in bid impressions - * T Advertising Bid Adapter: add keepalive option to notification fallback - * T Advertising Bid Adapter: fix indentation for linter - * T Advertising Bid Adapter: add support for ext.eid -
1 parent f019cb2 commit da680b2

3 files changed

Lines changed: 1318 additions & 0 deletions

File tree

modules/tadvertisingBidAdapter.js

Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
import {
2+
deepAccess,
3+
isEmpty,
4+
deepSetValue,
5+
logWarn,
6+
replaceAuctionPrice,
7+
triggerPixel,
8+
logError,
9+
isFn,
10+
isPlainObject,
11+
isInteger
12+
} from '../src/utils.js';
13+
import {registerBidder} from '../src/adapters/bidderFactory.js';
14+
import {BANNER, VIDEO} from "../src/mediaTypes.js";
15+
import {ortbConverter} from '../libraries/ortbConverter/converter.js';
16+
import {hasPurpose1Consent} from '../src/utils/gdpr.js';
17+
import {ajax, sendBeacon} from "../src/ajax.js";
18+
19+
const BIDDER_CODE = 'tadvertising';
20+
const GVL_ID = 213;
21+
const ENDPOINT_URL = 'https://prebid.tads.xplosion.de/bid';
22+
const NOTIFICATION_URL = 'https://prebid.tads.xplosion.de/notify';
23+
const USER_SYNC_URL = 'https://match.adsrvr.org/track/cmf/generic?ttd_pid=pxpinp0&ttd_tpi=1';
24+
const BID_TTL = 360;
25+
26+
const MEDIA_TYPES = {
27+
[BANNER]: 1,
28+
[VIDEO]: 2,
29+
};
30+
31+
const pageCache = {};
32+
33+
const converter = ortbConverter({
34+
bidResponse: (buildBidResponse, bid, context) => {
35+
let mediaType = BANNER;
36+
if (bid.adm && bid.adm.startsWith('<VAST')) {
37+
mediaType = VIDEO;
38+
}
39+
bid.mtype = MEDIA_TYPES[mediaType];
40+
41+
return buildBidResponse(bid, context);
42+
},
43+
});
44+
45+
export function buildSuccessNotification(bidEvent) {
46+
return Object.fromEntries(
47+
Object.entries({
48+
publisherId: deepAccess(bidEvent, 'params.0.publisherId'),
49+
placementId: deepAccess(bidEvent, 'params.0.placementId'),
50+
bidId: bidEvent.adId,
51+
auctionId: bidEvent.auctionId,
52+
adUnitCode: bidEvent.adUnitCode,
53+
page: pageCache[bidEvent.requestId],
54+
cpm: bidEvent.cpm,
55+
currency: bidEvent.currency,
56+
adId: bidEvent.adId,
57+
creativeId: bidEvent.creativeId,
58+
size: bidEvent.size,
59+
dealId: bidEvent.dealId,
60+
mediaType: bidEvent.mediaType,
61+
status: bidEvent.status,
62+
ttr: bidEvent.timeToRespond
63+
}).filter(([_, value]) => value != null)
64+
);
65+
}
66+
67+
export function buildErrorNotification(bidEvent, error = null) {
68+
return Object.fromEntries(
69+
Object.entries({
70+
publisherId: deepAccess(bidEvent, 'bids.0.params.publisherId') || deepAccess(bidEvent, 'bids.0.params.0.publisherId'),
71+
placementId: deepAccess(bidEvent, 'bids.0.params.placementId') || deepAccess(bidEvent, 'bids.0.params.0.placementId'),
72+
bidId: deepAccess(bidEvent, 'bids.0.bidId'),
73+
auctionId: deepAccess(bidEvent, 'auctionId'),
74+
adUnitCode: deepAccess(bidEvent, 'bids.0.adUnitCode'),
75+
page: deepAccess(bidEvent, 'refererInfo.page'),
76+
timeout: bidEvent.timeout,
77+
timedOut: error?.timedOut,
78+
statusCode: error?.status,
79+
response: error?.responseText
80+
}).filter(([_, value]) => value != null)
81+
);
82+
}
83+
84+
export function buildTimeoutNotification(bidEvent) {
85+
return Object.fromEntries(
86+
Object.entries({
87+
publisherId: deepAccess(bidEvent, 'params.0.publisherId'),
88+
placementId: deepAccess(bidEvent, 'params.0.placementId'),
89+
bidId: deepAccess(bidEvent, 'bidId'),
90+
auctionId: deepAccess(bidEvent, 'auctionId'),
91+
adUnitCode: deepAccess(bidEvent, 'adUnitCode'),
92+
page: deepAccess(bidEvent, 'ortb2.site.page'),
93+
timeout: deepAccess(bidEvent, 'timeout'),
94+
}).filter(([_, value]) => value != null)
95+
);
96+
}
97+
98+
export function getBidFloor (bid) {
99+
// value from params takes precedance over value set by Floor Module
100+
if (bid.params.bidfloor) {
101+
return bid.params.bidfloor;
102+
}
103+
104+
if (!isFn(bid.getFloor)) {
105+
return null;
106+
}
107+
108+
let floor = bid.getFloor({
109+
currency: 'USD',
110+
mediaType: '*',
111+
size: '*'
112+
});
113+
if (isPlainObject(floor) && !isNaN(floor.floor) && floor.currency === 'USD') {
114+
return floor.floor;
115+
}
116+
return null;
117+
}
118+
119+
export const sendNotification = (notifyUrl, eventType, data) => {
120+
try {
121+
const notificationUrl = `${notifyUrl}/${eventType}`;
122+
const payload = JSON.stringify(data)
123+
124+
if (!sendBeacon(notificationUrl, payload)) {
125+
// Fallback to using AJAX if Beacon API is not supported
126+
ajax(notificationUrl, null, payload, {
127+
method: 'POST',
128+
contentType: 'text/plain',
129+
keepalive: true,
130+
});
131+
}
132+
} catch (error) {
133+
logError(BIDDER_CODE, `Failed to notify event: ${eventType}`, error);
134+
}
135+
}
136+
137+
export const spec = {
138+
code: BIDDER_CODE,
139+
gvlid: GVL_ID,
140+
supportedMediaTypes: [BANNER, VIDEO],
141+
sync_url: USER_SYNC_URL,
142+
notify_url: NOTIFICATION_URL,
143+
144+
isBidRequestValid: function (bid) {
145+
if (!bid.params.publisherId) {
146+
logWarn(BIDDER_CODE + ': Missing required parameter params.publisherId');
147+
return false;
148+
}
149+
if (bid.params.publisherId.length > 32) {
150+
logWarn(BIDDER_CODE + ': params.publisherId must be 32 characters or less');
151+
return false;
152+
}
153+
if (!bid.params.placementId) {
154+
logWarn(BIDDER_CODE + ': Missing required parameter params.placementId');
155+
return false;
156+
}
157+
158+
const mediaTypesBanner = deepAccess(bid, 'mediaTypes.banner');
159+
const mediaTypesVideo = deepAccess(bid, 'mediaTypes.video');
160+
161+
if (!mediaTypesBanner && !mediaTypesVideo) {
162+
logWarn(BIDDER_CODE + ': one of mediaTypes.banner or mediaTypes.video must be passed');
163+
return false;
164+
}
165+
166+
if (FEATURES.VIDEO && mediaTypesVideo) {
167+
if (!mediaTypesVideo.maxduration || !isInteger(mediaTypesVideo.maxduration)) {
168+
logWarn(BIDDER_CODE + ': mediaTypes.video.maxduration must be set to the maximum video ad duration in seconds');
169+
return false;
170+
}
171+
if (!mediaTypesVideo.api || mediaTypesVideo.api.length === 0) {
172+
logWarn(BIDDER_CODE + ': mediaTypes.video.api should be an array of supported api frameworks. See the Open RTB v2.5 spec for valid values');
173+
return false;
174+
}
175+
if (!mediaTypesVideo.mimes || mediaTypesVideo.mimes.length === 0) {
176+
logWarn(BIDDER_CODE + ': mediaTypes.video.mimes should be an array of supported mime types');
177+
return false;
178+
}
179+
if (!mediaTypesVideo.protocols) {
180+
logWarn(BIDDER_CODE + ': mediaTypes.video.protocols should be an array of supported protocols. See the Open RTB v2.5 spec for valid values');
181+
return false;
182+
}
183+
}
184+
return true;
185+
},
186+
187+
buildRequests: function (validBidRequests, bidderRequest) {
188+
let data = converter.toORTB({validBidRequests, bidderRequest})
189+
deepSetValue(data, 'site.publisher.id', bidderRequest.bids[0].params.publisherId)
190+
191+
const bidFloor = getBidFloor(bidderRequest.bids[0])
192+
if (bidFloor) {
193+
deepSetValue(data, 'imp.0.bidfloor', bidFloor)
194+
deepSetValue(data, 'imp.0.bidfloorcur', 'USD')
195+
}
196+
197+
if (deepAccess(validBidRequests[0], 'userIdAsEids')) {
198+
deepSetValue(data, 'user.ext.eids', validBidRequests[0].userIdAsEids);
199+
}
200+
201+
bidderRequest.bids.forEach((bid, index) => {
202+
pageCache[bid.bidId] = deepAccess(bid, 'ortb2.site.page');
203+
deepSetValue(data, `imp.${index}.ext.gpid`, bid.params.placementId);
204+
})
205+
return {
206+
method: 'POST',
207+
url: ENDPOINT_URL,
208+
data: data,
209+
};
210+
},
211+
212+
interpretResponse: function (response, serverRequest) {
213+
if (isEmpty(response.body)) {
214+
return [];
215+
}
216+
deepSetValue(response, 'body.seatbid.0.bid.0.impid', deepAccess(serverRequest, 'data.imp.0.id'))
217+
218+
const bids = converter.fromORTB({response: response.body, request: serverRequest.data}).bids;
219+
220+
bids.forEach(bid => {
221+
bid.ttl = BID_TTL;
222+
bid.netRevenue = true;
223+
bid.currency = bid.currency || 'USD';
224+
bid.dealId = bid.dealId || null;
225+
if (bid.vastXml) {
226+
bid.vastXml = replaceAuctionPrice(bid.vastXml, bid.cpm);
227+
} else {
228+
bid.ad = replaceAuctionPrice(bid.ad, bid.cpm);
229+
}
230+
})
231+
232+
return bids;
233+
},
234+
235+
getUserSyncs: function (syncOptions, serverResponses, gdprConsent) {
236+
const syncs = []
237+
if (serverResponses[0]?.body?.ext?.uss === 1 && gdprConsent && hasPurpose1Consent(gdprConsent)) {
238+
let gdprParams;
239+
if (typeof gdprConsent.gdprApplies === 'boolean') {
240+
gdprParams = `&gdpr=${Number(gdprConsent.gdprApplies)}&gdpr_consent=${gdprConsent.consentString}`;
241+
} else {
242+
gdprParams = `&gdpr_consent=${gdprConsent.consentString}`;
243+
}
244+
245+
if (syncOptions.pixelEnabled) {
246+
syncs.push({
247+
type: 'image',
248+
url: USER_SYNC_URL + gdprParams
249+
});
250+
}
251+
}
252+
return syncs;
253+
},
254+
255+
onBidWon: function (bid) {
256+
const payload = buildSuccessNotification(bid)
257+
sendNotification(spec.notify_url, "won", payload)
258+
},
259+
260+
onBidBillable: function (bid) {
261+
if (bid.burl) {
262+
triggerPixel(replaceAuctionPrice(bid.burl, bid.cpm));
263+
}
264+
const payload = buildSuccessNotification(bid)
265+
sendNotification(spec.notify_url, "billable", payload)
266+
},
267+
268+
onTimeout: function (timeoutData) {
269+
const payload = timeoutData.map(data => buildTimeoutNotification(data))
270+
sendNotification(spec.notify_url, 'timeout', payload)
271+
},
272+
273+
onBidderError: function ({error, bidderRequest}) {
274+
const payload = buildErrorNotification(bidderRequest, error)
275+
sendNotification(spec.notify_url, 'error', payload)
276+
}
277+
}
278+
279+
registerBidder(spec);

modules/tadvertisingBidAdapter.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Overview
2+
3+
```markdown
4+
Module Name: T-Advertising Solutions Bid Adapter
5+
Module Type: Bidder Adapter
6+
Maintainer: dev@emetriq.com
7+
```
8+
9+
# Description
10+
The T-Advertising Solutions Bid Adapter is a module that connects to T-Advertising Solutions demand sources, enabling
11+
publishers to access advertising demand. This adapter facilitates real-time bidding integration between Prebid.js and
12+
T-Advertising Solutions' platform.
13+
14+
This adapter supports both Banner and Video ad formats
15+
16+
# Test Parameters
17+
The following ad units demonstrate how to configure the adapter for different ad formats:
18+
19+
## Banner Ad Unit Example
20+
```javascript
21+
var bannerAdUnit = {
22+
code: 'myBannerAdUnit',
23+
mediaTypes: {
24+
banner: {
25+
sizes: [400, 600],
26+
}
27+
},
28+
bids: [
29+
{
30+
bidder: 'tadvertising',
31+
params: {
32+
publisherId: '1427ab10f2e448057ed3b422',
33+
placementId: 'sidebar_1',
34+
bidfloor: 0.95 // Optional - default is 0
35+
}
36+
}
37+
]
38+
};
39+
```
40+
41+
The banner ad unit configuration above demonstrates how to set up a basic banner implementation.
42+
43+
## Video Ad Unit Example
44+
```javascript
45+
var videoAdUnit = {
46+
code: 'myVideoAdUnit',
47+
mediaTypes: {
48+
video: {
49+
mimes: ['video/mp4'],
50+
minduration: 1,
51+
maxduration: 60,
52+
api: [1, 3],
53+
placement: 3,
54+
protocols: [2,3,5,6]
55+
}
56+
},
57+
bids: [
58+
{
59+
bidder: "tadvertising",
60+
params: {
61+
publisherId: '1427ab10f2e448057ed3b422',
62+
placementId: 'sidebar_1',
63+
bidfloor: 0.95 // Optional - default is 0
64+
}
65+
}
66+
]
67+
}
68+
```
69+
The video ad unit configuration demonstrates how to set up a basic video implementation.
70+
71+
# GDPR Compliance
72+
73+
The T-Advertising Solutions adapter supports the IAB Europe Transparency & Consent Framework (TCF) for GDPR compliance.
74+
When properly configured, the adapter will pass consent information to T-Advertising Solutions' servers.

0 commit comments

Comments
 (0)