Skip to content

Commit 344cb98

Browse files
TheTradeDesk: Add Bidder (#3370)
1 parent f486ca5 commit 344cb98

12 files changed

Lines changed: 944 additions & 0 deletions

File tree

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
package org.prebid.server.bidder.thetradedesk;
2+
3+
import com.fasterxml.jackson.core.type.TypeReference;
4+
import com.iab.openrtb.request.App;
5+
import com.iab.openrtb.request.Banner;
6+
import com.iab.openrtb.request.BidRequest;
7+
import com.iab.openrtb.request.Format;
8+
import com.iab.openrtb.request.Imp;
9+
import com.iab.openrtb.request.Publisher;
10+
import com.iab.openrtb.request.Site;
11+
import com.iab.openrtb.response.Bid;
12+
import com.iab.openrtb.response.BidResponse;
13+
import com.iab.openrtb.response.SeatBid;
14+
import io.vertx.core.MultiMap;
15+
import org.apache.commons.collections4.CollectionUtils;
16+
import org.apache.commons.lang3.StringUtils;
17+
import org.prebid.server.bidder.Bidder;
18+
import org.prebid.server.bidder.model.BidderBid;
19+
import org.prebid.server.bidder.model.BidderCall;
20+
import org.prebid.server.bidder.model.BidderError;
21+
import org.prebid.server.bidder.model.HttpRequest;
22+
import org.prebid.server.bidder.model.Result;
23+
import org.prebid.server.exception.PreBidException;
24+
import org.prebid.server.json.DecodeException;
25+
import org.prebid.server.json.JacksonMapper;
26+
import org.prebid.server.proto.openrtb.ext.ExtPrebid;
27+
import org.prebid.server.proto.openrtb.ext.request.thetradedesk.ExtImpTheTradeDesk;
28+
import org.prebid.server.proto.openrtb.ext.response.BidType;
29+
import org.prebid.server.util.BidderUtil;
30+
import org.prebid.server.util.HttpUtil;
31+
32+
import java.util.ArrayList;
33+
import java.util.Collection;
34+
import java.util.Collections;
35+
import java.util.List;
36+
import java.util.Objects;
37+
import java.util.regex.Pattern;
38+
39+
public class TheTradeDeskBidder implements Bidder<BidRequest> {
40+
41+
private static final TypeReference<ExtPrebid<?, ExtImpTheTradeDesk>> TYPE_REFERENCE =
42+
new TypeReference<>() {
43+
};
44+
45+
private static final String PREBID_INTEGRATION_TYPE_HEADER = "x-integration-type";
46+
private static final String PREBID_INTEGRATION_TYPE = "1";
47+
private static final MultiMap HEADERS = HttpUtil.headers()
48+
.add(PREBID_INTEGRATION_TYPE_HEADER, PREBID_INTEGRATION_TYPE);
49+
50+
private static final String SUPPLY_ID_MACRO = "{{SupplyId}}";
51+
private static final Pattern SUPPLY_ID_PATTERN = Pattern.compile("([a-z]+)$");
52+
53+
private final String endpointUrl;
54+
private final String supplyId;
55+
private final JacksonMapper mapper;
56+
57+
public TheTradeDeskBidder(String endpointUrl, JacksonMapper mapper, String supplyId) {
58+
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
59+
this.supplyId = validateSupplyId(supplyId);
60+
this.mapper = Objects.requireNonNull(mapper);
61+
}
62+
63+
private static String validateSupplyId(String supplyId) {
64+
if (StringUtils.isBlank(supplyId) || SUPPLY_ID_PATTERN.matcher(supplyId).matches()) {
65+
return supplyId;
66+
}
67+
68+
throw new IllegalArgumentException("SupplyId must be a simple string provided by TheTradeDesk");
69+
}
70+
71+
@Override
72+
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
73+
final List<Imp> modifiedImps = new ArrayList<>();
74+
75+
String publisherId = null;
76+
for (Imp imp : request.getImp()) {
77+
try {
78+
final ExtImpTheTradeDesk extImp = parseImpExt(imp);
79+
publisherId = publisherId == null
80+
? StringUtils.isNotBlank(extImp.getPublisherId())
81+
? extImp.getPublisherId()
82+
: publisherId
83+
: publisherId;
84+
85+
modifiedImps.add(modifyImp(imp));
86+
} catch (PreBidException e) {
87+
return Result.withError(BidderError.badInput(e.getMessage()));
88+
}
89+
}
90+
91+
final BidRequest outgoingRequest = modifyRequest(request, modifiedImps, publisherId);
92+
final HttpRequest<BidRequest> httpRequest = BidderUtil.defaultRequest(
93+
outgoingRequest,
94+
HEADERS,
95+
resolveEndpoint(),
96+
mapper);
97+
98+
return Result.withValue(httpRequest);
99+
}
100+
101+
private ExtImpTheTradeDesk parseImpExt(Imp imp) {
102+
try {
103+
return mapper.mapper().convertValue(imp.getExt(), TYPE_REFERENCE).getBidder();
104+
} catch (IllegalArgumentException e) {
105+
throw new PreBidException(e.getMessage(), e);
106+
}
107+
}
108+
109+
private static Imp modifyImp(Imp imp) {
110+
final Banner banner = imp.getBanner();
111+
112+
if (banner != null && CollectionUtils.isNotEmpty(banner.getFormat())) {
113+
final Format format = banner.getFormat().getFirst();
114+
return imp.toBuilder()
115+
.banner(banner.toBuilder().w(format.getW()).h(format.getH()).build())
116+
.build();
117+
}
118+
119+
return imp;
120+
}
121+
122+
private static BidRequest modifyRequest(BidRequest request, List<Imp> modifiedImps, String publisherId) {
123+
return request.toBuilder()
124+
.imp(modifiedImps)
125+
.site(modifySite(request, publisherId))
126+
.app(modifyApp(request, publisherId))
127+
.build();
128+
}
129+
130+
private static Site modifySite(BidRequest request, String publisherId) {
131+
final Site site = request.getSite();
132+
if (site == null) {
133+
return null;
134+
}
135+
136+
return site.toBuilder()
137+
.publisher(modifyPublisher(site.getPublisher(), publisherId))
138+
.build();
139+
}
140+
141+
private static Publisher modifyPublisher(Publisher publisher, String publisherId) {
142+
if (publisher == null) {
143+
return Publisher.builder().id(publisherId).build();
144+
}
145+
146+
return publisher.toBuilder()
147+
.id(StringUtils.isNotBlank(publisherId) ? publisherId : publisher.getId())
148+
.build();
149+
}
150+
151+
private static App modifyApp(BidRequest request, String publisherId) {
152+
final Site site = request.getSite();
153+
final App app = request.getApp();
154+
155+
if (site != null) {
156+
return app;
157+
}
158+
159+
if (app == null) {
160+
return null;
161+
}
162+
163+
return app.toBuilder()
164+
.publisher(modifyPublisher(app.getPublisher(), publisherId))
165+
.build();
166+
}
167+
168+
private String resolveEndpoint() {
169+
return endpointUrl.replace(SUPPLY_ID_MACRO, HttpUtil.encodeUrl(StringUtils.defaultString(supplyId)));
170+
}
171+
172+
@Override
173+
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
174+
try {
175+
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
176+
return Result.withValues(extractBids(bidResponse));
177+
} catch (DecodeException | PreBidException e) {
178+
return Result.withError(BidderError.badServerResponse(e.getMessage()));
179+
}
180+
}
181+
182+
private static List<BidderBid> extractBids(BidResponse bidResponse) {
183+
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) {
184+
return Collections.emptyList();
185+
}
186+
187+
return bidResponse.getSeatbid().stream()
188+
.filter(Objects::nonNull)
189+
.map(SeatBid::getBid).filter(Objects::nonNull)
190+
.flatMap(Collection::stream)
191+
.filter(Objects::nonNull)
192+
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur()))
193+
.toList();
194+
}
195+
196+
private static BidType getBidType(Bid bid) {
197+
return switch (bid.getMtype()) {
198+
case 1 -> BidType.banner;
199+
case 2 -> BidType.video;
200+
case 4 -> BidType.xNative;
201+
case null, default -> throw new PreBidException("unsupported mtype: %s".formatted(bid.getMtype()));
202+
};
203+
}
204+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package org.prebid.server.proto.openrtb.ext.request.thetradedesk;
2+
3+
import com.fasterxml.jackson.annotation.JsonProperty;
4+
import lombok.Value;
5+
6+
@Value(staticConstructor = "of")
7+
public class ExtImpTheTradeDesk {
8+
9+
@JsonProperty("publisherId")
10+
String publisherId;
11+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
package org.prebid.server.spring.config.bidder;
2+
3+
import lombok.Data;
4+
import lombok.EqualsAndHashCode;
5+
import lombok.NoArgsConstructor;
6+
import org.prebid.server.bidder.BidderDeps;
7+
import org.prebid.server.bidder.thetradedesk.TheTradeDeskBidder;
8+
import org.prebid.server.json.JacksonMapper;
9+
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties;
10+
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler;
11+
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator;
12+
import org.prebid.server.spring.env.YamlPropertySourceFactory;
13+
import org.springframework.beans.factory.annotation.Value;
14+
import org.springframework.boot.context.properties.ConfigurationProperties;
15+
import org.springframework.context.annotation.Bean;
16+
import org.springframework.context.annotation.Configuration;
17+
import org.springframework.context.annotation.PropertySource;
18+
19+
import jakarta.validation.constraints.NotBlank;
20+
21+
@Configuration
22+
@PropertySource(value = "classpath:/bidder-config/thetradedesk.yaml", factory = YamlPropertySourceFactory.class)
23+
public class TheTradeDeskConfiguration {
24+
25+
private static final String BIDDER_NAME = "thetradedesk";
26+
27+
@Bean("thetradedeskConfigurationProperties")
28+
@ConfigurationProperties("adapters.thetradedesk")
29+
TheTradeDeskConfigurationProperties configurationProperties() {
30+
return new TheTradeDeskConfigurationProperties();
31+
}
32+
33+
@Bean
34+
BidderDeps theTradeDeskBidderDeps(TheTradeDeskConfigurationProperties theTradeDeskConfigurationProperties,
35+
@NotBlank @Value("${external-url}") String externalUrl,
36+
JacksonMapper mapper) {
37+
38+
return BidderDepsAssembler.<TheTradeDeskConfigurationProperties>forBidder(BIDDER_NAME)
39+
.withConfig(theTradeDeskConfigurationProperties)
40+
.usersyncerCreator(UsersyncerCreator.create(externalUrl))
41+
.bidderCreator(config -> new TheTradeDeskBidder(
42+
config.getEndpoint(),
43+
mapper,
44+
config.getExtraInfo().getSupplyId())
45+
).assemble();
46+
}
47+
48+
@Data
49+
@EqualsAndHashCode(callSuper = true)
50+
@NoArgsConstructor
51+
private static class TheTradeDeskConfigurationProperties extends BidderConfigurationProperties {
52+
53+
private ExtraInfo extraInfo = new ExtraInfo();
54+
}
55+
56+
@Data
57+
@NoArgsConstructor
58+
private static class ExtraInfo {
59+
60+
String supplyId;
61+
}
62+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
adapters:
2+
thetradedesk:
3+
endpoint: https://direct.adsrvr.org/bid/bidder/{{SupplyId}}
4+
meta-info:
5+
maintainer-email: Prebid-Maintainers@thetradedesk.com
6+
app-media-types:
7+
- banner
8+
- video
9+
- native
10+
site-media-types:
11+
- banner
12+
- video
13+
- native
14+
supported-vendors:
15+
vendor-id: 21
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-04/schema#",
3+
"title": "The Trade Desk Adapter Params",
4+
"description": "A schema which validates params accepted by the The Trade Desk adapter",
5+
"type": "object",
6+
"properties": {
7+
"publisherId": {
8+
"type": "string",
9+
"description": "An ID which identifies the publisher"
10+
}
11+
},
12+
"required": [
13+
"publisherId"
14+
]
15+
}

0 commit comments

Comments
 (0)