-
Notifications
You must be signed in to change notification settings - Fork 190
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
Connatix: Port adapter from PBS-Go #3781
base: master
Are you sure you want to change the base?
Changes from 7 commits
38abb67
ff9a8d9
c8d9838
816c3f5
7f6042e
4597547
a5e3659
583e7ce
764f2c4
072c7bf
555dd26
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
package org.prebid.server.bidder.connatix; | ||
|
||
import com.fasterxml.jackson.core.type.TypeReference; | ||
import com.fasterxml.jackson.databind.JsonNode; | ||
import com.fasterxml.jackson.databind.node.ObjectNode; | ||
import com.iab.openrtb.request.App; | ||
import com.iab.openrtb.request.Banner; | ||
import com.iab.openrtb.request.BidRequest; | ||
import com.iab.openrtb.request.Device; | ||
import com.iab.openrtb.request.Format; | ||
import com.iab.openrtb.request.Imp; | ||
import com.iab.openrtb.response.Bid; | ||
import com.iab.openrtb.response.BidResponse; | ||
import com.iab.openrtb.response.SeatBid; | ||
import io.vertx.core.MultiMap; | ||
import org.apache.commons.collections4.CollectionUtils; | ||
import org.apache.commons.collections4.ListUtils; | ||
import org.apache.commons.lang3.ObjectUtils; | ||
import org.apache.commons.lang3.StringUtils; | ||
import org.prebid.server.bidder.Bidder; | ||
import org.prebid.server.bidder.connatix.proto.ConnatixImpExtBidder; | ||
import org.prebid.server.bidder.model.BidderBid; | ||
import org.prebid.server.bidder.model.BidderCall; | ||
import org.prebid.server.bidder.model.BidderError; | ||
import org.prebid.server.bidder.model.HttpRequest; | ||
import org.prebid.server.bidder.model.Price; | ||
import org.prebid.server.bidder.model.Result; | ||
import org.prebid.server.currency.CurrencyConversionService; | ||
import org.prebid.server.exception.PreBidException; | ||
import org.prebid.server.json.DecodeException; | ||
import org.prebid.server.json.JacksonMapper; | ||
import org.prebid.server.proto.openrtb.ext.ExtPrebid; | ||
import org.prebid.server.proto.openrtb.ext.request.ExtApp; | ||
import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid; | ||
import org.prebid.server.proto.openrtb.ext.request.connatix.ExtImpConnatix; | ||
import org.prebid.server.proto.openrtb.ext.response.BidType; | ||
import org.prebid.server.util.BidderUtil; | ||
import org.prebid.server.util.HttpUtil; | ||
|
||
import java.math.BigDecimal; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import java.util.Objects; | ||
import java.util.Optional; | ||
|
||
public class ConnatixBidder implements Bidder<BidRequest> { | ||
|
||
private static final TypeReference<ExtPrebid<?, ExtImpConnatix>> CONNATIX_EXT_TYPE_REFERENCE = | ||
new TypeReference<>() { | ||
}; | ||
|
||
private static final int MAX_IMPS_PER_REQUEST = 1; | ||
|
||
private final String endpointUrl; | ||
private final JacksonMapper mapper; | ||
|
||
private static final String BIDDER_CURRENCY = "USD"; | ||
|
||
private final CurrencyConversionService currencyConversionService; | ||
|
||
public ConnatixBidder(String endpointUrl, | ||
CurrencyConversionService currencyConversionService, | ||
JacksonMapper mapper) { | ||
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl)); | ||
this.currencyConversionService = currencyConversionService; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. requireNonNull |
||
this.mapper = Objects.requireNonNull(mapper); | ||
} | ||
|
||
@Override | ||
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please fix a method order according to the code style |
||
// Device IP required - bounce if not available | ||
if (request.getDevice() == null | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd extract device into a separate variable since it's used later as well |
||
|| (request.getDevice().getIp() == null && request.getDevice().getIpv6() == null)) { | ||
return Result.withError(BidderError.badInput("Device IP is required")); | ||
} | ||
|
||
final String displayManagerVer = buildDisplayManagerVersion(request); | ||
final MultiMap headers = resolveHeaders(request.getDevice()); | ||
|
||
final List<Imp> modifiedImps = new ArrayList<>(); | ||
final List<BidderError> errors = new ArrayList<>(); | ||
|
||
for (Imp imp : request.getImp()) { | ||
final ExtImpConnatix extImpConnatix; | ||
final Price bidFloorPrice; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. it doesn't make sense to separate initialization and assignment in this particular case |
||
try { | ||
extImpConnatix = parseExtImp(imp); | ||
bidFloorPrice = convertBidFloor(imp, request); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd move conversion of the bid floor inside the |
||
final Imp modifiedImp = modifyImp(imp, extImpConnatix, displayManagerVer, bidFloorPrice); | ||
|
||
modifiedImps.add(modifiedImp); | ||
} catch (PreBidException e) { | ||
errors.add(BidderError.badInput(e.getMessage())); | ||
} | ||
} | ||
|
||
if (modifiedImps.isEmpty()) { | ||
return Result.withErrors(errors); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this check looks redundant |
||
|
||
final List<HttpRequest<BidRequest>> httpRequests = splitHttpRequests(request, modifiedImps, headers); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't get the point of splitting, since it's literally one imp per request, this logic looks redundant so you can just create a request for each imp while iteration over them |
||
|
||
return Result.withValues(httpRequests); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. collected errors are missing here |
||
} | ||
|
||
private Imp modifyImp(Imp imp, ExtImpConnatix extImpConnatix, String displayManagerVer, Price bidFloorPrice) { | ||
final ConnatixImpExtBidder impExtBidder = resolveImpExt(extImpConnatix); | ||
|
||
final ObjectNode impExtBidderNode = mapper.mapper().valueToTree(impExtBidder); | ||
|
||
final ObjectNode modifiedImpExtBidder = imp.getExt() != null ? imp.getExt().deepCopy() | ||
: mapper.mapper().createObjectNode(); | ||
|
||
modifiedImpExtBidder.setAll(impExtBidderNode); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. final ObjectNode impExt = mapper.mapper().createObjectNode().set("connatix", mapper.mapper().valueToTree(extImpConnatix)); |
||
|
||
return imp.toBuilder() | ||
.ext(modifiedImpExtBidder) | ||
.banner(modifyImpBanner(imp.getBanner())) | ||
.displaymanagerver(!StringUtils.isEmpty(imp.getDisplaymanagerver()) | ||
? imp.getDisplaymanagerver() : displayManagerVer) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. StringUtils.isBlank(imp.getDisplaymanagerver()) && StringUtils.isNotBlank(displayManagerVer)
? displayManagerVer
: imp.getDisplaymanagerver() |
||
.bidfloor(bidFloorPrice.getValue()) | ||
.bidfloorcur(bidFloorPrice.getCurrency()) | ||
.build(); | ||
} | ||
|
||
private Banner modifyImpBanner(Banner banner) { | ||
if (banner == null) { | ||
return null; | ||
} | ||
|
||
if (banner.getW() == null && banner.getH() == null && !CollectionUtils.isEmpty(banner.getFormat())) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
final Format firstFormat = banner.getFormat().getFirst(); | ||
return banner.toBuilder() | ||
.w(firstFormat.getW()) | ||
.h(firstFormat.getH()) | ||
.build(); | ||
} | ||
return banner; | ||
} | ||
|
||
@Override | ||
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) { | ||
try { | ||
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); | ||
final List<BidderBid> bids = extractBids(httpCall.getRequest().getPayload(), bidResponse); | ||
|
||
return Result.withValues(bids); | ||
} catch (DecodeException | PreBidException e) { | ||
return Result.withError(BidderError.badServerResponse(e.getMessage())); | ||
} | ||
} | ||
|
||
private Price convertBidFloor(Imp imp, BidRequest bidRequest) { | ||
final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor()); | ||
if (BidderUtil.isValidPrice(initialBidFloorPrice)) { | ||
try { | ||
final BigDecimal convertedPrice = currencyConversionService | ||
.convertCurrency(imp.getBidfloor(), bidRequest, imp.getBidfloorcur(), BIDDER_CURRENCY); | ||
return Price.of(BIDDER_CURRENCY, convertedPrice); | ||
} catch (PreBidException e) { | ||
throw new PreBidException("Unable to convert provided bid floor currency from %s to %s for imp `%s`" | ||
.formatted(imp.getBidfloorcur(), BIDDER_CURRENCY, imp.getId())); | ||
} | ||
} | ||
return initialBidFloorPrice; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. private Price resolveBidFloor(Imp imp, BidRequest bidRequest) {
final Price initialBidFloorPrice = Price.of(imp.getBidfloorcur(), imp.getBidfloor());
return BidderUtil.shouldConvertBidFloor(initialBidFloorPrice, DEFAULT_BID_CURRENCY)
? convertBidFloor(initialBidFloorPrice, bidRequest)
: initialBidFloorPrice;
}
private Price convertBidFloor(Price bidFloorPrice, BidRequest bidRequest) {
final BigDecimal convertedPrice = currencyConversionService.convertCurrency(
bidFloorPrice.getValue(),
bidRequest,
bidFloorPrice.getCurrency(),
DEFAULT_BID_CURRENCY);
return Price.of(DEFAULT_BID_CURRENCY, convertedPrice); |
||
|
||
// extract bids | ||
private static List<BidderBid> extractBids(BidRequest bidRequest, BidResponse bidResponse) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is bidRequest used? |
||
if (bidResponse == null || CollectionUtils.isEmpty(bidResponse.getSeatbid())) { | ||
return Collections.emptyList(); | ||
} | ||
|
||
return bidResponse.getSeatbid().stream() | ||
.filter(Objects::nonNull) | ||
.map(SeatBid::getBid) | ||
.filter(Objects::nonNull) | ||
.flatMap(Collection::stream) | ||
.filter(Objects::nonNull) | ||
.map(bid -> BidderBid.of(bid, getBidType(bid), bidResponse.getCur())) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
.toList(); | ||
} | ||
|
||
// parseExtImp | ||
private ExtImpConnatix parseExtImp(Imp imp) { | ||
try { | ||
return mapper.mapper().convertValue(imp.getExt(), CONNATIX_EXT_TYPE_REFERENCE).getBidder(); | ||
} catch (IllegalArgumentException e) { | ||
throw new PreBidException(e.getMessage()); | ||
} | ||
} | ||
|
||
private ConnatixImpExtBidder resolveImpExt(ExtImpConnatix extImpConnatix) { | ||
final ConnatixImpExtBidder.ConnatixImpExtBidderBuilder builder = ConnatixImpExtBidder.builder(); | ||
if (StringUtils.isNotEmpty(extImpConnatix.getPlacementId())) { | ||
builder.placementId(extImpConnatix.getPlacementId()); | ||
} | ||
if (extImpConnatix.getViewabilityPercentage() != null) { | ||
builder.viewabilityPercentage(extImpConnatix.getViewabilityPercentage()); | ||
} | ||
|
||
return builder.build(); | ||
} | ||
|
||
private HttpRequest<BidRequest> makeHttpRequest(BidRequest request, List<Imp> impsChunk, MultiMap headers) { | ||
final BidRequest outgoingRequest = request.toBuilder() | ||
.imp(impsChunk) | ||
.cur(List.of(BIDDER_CURRENCY)) | ||
.build(); | ||
|
||
return BidderUtil.defaultRequest(outgoingRequest, headers, endpointUrl, mapper); | ||
} | ||
|
||
private List<HttpRequest<BidRequest>> splitHttpRequests(BidRequest bidRequest, | ||
List<Imp> imps, | ||
MultiMap headers) { | ||
return ListUtils.partition(imps, MAX_IMPS_PER_REQUEST) | ||
.stream() | ||
.map(impsChunk -> makeHttpRequest(bidRequest, impsChunk, headers)) | ||
.toList(); | ||
} | ||
|
||
private MultiMap resolveHeaders(Device device) { | ||
final MultiMap headers = HttpUtil.headers(); | ||
if (device != null) { | ||
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); | ||
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIpv6()); | ||
HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); | ||
} | ||
return headers; | ||
} | ||
|
||
// check display manager version | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. remove comments |
||
private String buildDisplayManagerVersion(BidRequest request) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. static |
||
final Optional<ExtAppPrebid> prebid = Optional.ofNullable(request.getApp()) | ||
.map(App::getExt) | ||
.map(ExtApp::getPrebid); | ||
|
||
final String source = prebid.map(ExtAppPrebid::getSource).orElse(null); | ||
final String version = prebid.map(ExtAppPrebid::getVersion).orElse(null); | ||
|
||
return ObjectUtils.allNotNull(source, version) | ||
? "%s-%s".formatted(source, version) | ||
: ""; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return Optional.ofNullable(request.getApp())
.map(App::getExt)
.map(ExtApp::getPrebid)
.filter(prebid -> ObjectUtils.allNotNull(prebid.getSource(), prebid.getVersion()))
.map(prebid -> "%s-%s".formatted(prebid.getSource(), prebid.getVersion())
.orElse(StringUtils.EMPTY); also "%s-%s" can be extracted as a constant |
||
} | ||
|
||
private static BidType getBidType(Bid bid) { | ||
if (bid == null) { | ||
return null; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. a bid can't be null |
||
final Optional<String> bidType = Optional.ofNullable(bid.getExt()) | ||
.map(ext -> ext.get("connatix")) | ||
.map(cnx -> cnx.get("mediaType")) | ||
.map(JsonNode::asText); | ||
if (bidType.isPresent() && bidType.get().equals("video")) { | ||
return BidType.video; | ||
} | ||
return BidType.banner; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. return Optional.ofNullable(bid.getExt())
.map(ext -> ext.get("connatix"))
.map(cnx -> cnx.get("mediaType"))
.map(JsonNode::asText)
.filter(type -> Objects.equals(type, "video"))
.map(ignored -> BidType.video)
.orElse(BidType.banner); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. also please let the Go team know they probably have an issue in bidType resolving since the following doesn't make any sense if err := jsonutil.Unmarshal(bid.Ext, &bidExt); err != nil {
bidType = openrtb_ext.BidTypeBanner
} else {
bidType = getBidType(bidExt)
} |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
package org.prebid.server.bidder.connatix.proto; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Builder; | ||
import lombok.Value; | ||
|
||
@Builder | ||
@Value | ||
public class ConnatixImpExtBidder { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redundant |
||
|
||
String type; | ||
|
||
@JsonProperty(value = "placementId") | ||
String placementId; | ||
|
||
@JsonProperty(value = "viewabilityPercentage") | ||
Float viewabilityPercentage; | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.prebid.server.proto.openrtb.ext.request.connatix; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Builder; | ||
import lombok.Value; | ||
|
||
@Builder(toBuilder = true) | ||
@Value(staticConstructor = "of") | ||
public class ExtImpConnatix { | ||
|
||
@JsonProperty("placementId") | ||
String placementId; | ||
|
||
@JsonProperty("viewabilityPercentage") | ||
Float viewabilityPercentage; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BigDecimal |
||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
package org.prebid.server.spring.config.bidder; | ||
|
||
import org.prebid.server.bidder.BidderDeps; | ||
import org.prebid.server.bidder.connatix.ConnatixBidder; | ||
import org.prebid.server.currency.CurrencyConversionService; | ||
import org.prebid.server.json.JacksonMapper; | ||
import org.prebid.server.spring.config.bidder.model.BidderConfigurationProperties; | ||
import org.prebid.server.spring.config.bidder.util.BidderDepsAssembler; | ||
import org.prebid.server.spring.config.bidder.util.UsersyncerCreator; | ||
import org.prebid.server.spring.env.YamlPropertySourceFactory; | ||
import org.springframework.beans.factory.annotation.Value; | ||
import org.springframework.boot.context.properties.ConfigurationProperties; | ||
import org.springframework.context.annotation.Bean; | ||
import org.springframework.context.annotation.Configuration; | ||
import org.springframework.context.annotation.PropertySource; | ||
|
||
import jakarta.validation.constraints.NotBlank; | ||
|
||
@Configuration | ||
@PropertySource(value = "classpath:/bidder-config/connatix.yaml", factory = YamlPropertySourceFactory.class) | ||
public class ConnatixConfiguration { | ||
|
||
private static final String BIDDER_NAME = "connatix"; | ||
|
||
@Bean("connatixConfigurationProperties") | ||
@ConfigurationProperties("adapters.connatix") | ||
BidderConfigurationProperties configurationProperties() { | ||
return new BidderConfigurationProperties(); | ||
} | ||
|
||
@Bean | ||
BidderDeps connatixBidderDeps(BidderConfigurationProperties connatixConfigurationProperties, | ||
@NotBlank @Value("${external-url}") String externalUrl, | ||
JacksonMapper mapper, | ||
CurrencyConversionService currencyConversionService) { | ||
|
||
return BidderDepsAssembler.forBidder(BIDDER_NAME) | ||
.withConfig(connatixConfigurationProperties) | ||
.usersyncerCreator(UsersyncerCreator.create(externalUrl)) | ||
.bidderCreator(config -> new ConnatixBidder(config.getEndpoint(), currencyConversionService, mapper)) | ||
.assemble(); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
adapters: | ||
connatix: | ||
endpoint: "https://capi.connatix.com/rtb/ortb" | ||
ortb: | ||
multiformat-supported: false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. redundant |
||
meta-info: | ||
maintainer-email: "[email protected]" | ||
vendor-id: 143 | ||
app-media-types: | ||
- banner | ||
- video | ||
site-media-types: | ||
- banner | ||
- video | ||
usersync: | ||
cookie-family-name: connatix | ||
iframe: | ||
url: "https://capi.connatix.com/us/pixel?pId=53&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback={{redirect_url}}" | ||
uid-macro: '$UID' | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. '[UID]' |
||
supportCors: false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. support-cors There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ^^ |
||
redirect: | ||
url: "https://capi.connatix.com/us/pixel?pId=52&gdpr={{gdpr}}&gdpr_consent={{gdpr_consent}}&us_privacy={{us_privacy}}&callback={{redirect_url}}" | ||
uid-macro: '$UID' | ||
supportCors: false | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. support-cors: |
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
{ | ||
"$schema": "http://json-schema.org/draft-04/schema#", | ||
"title": "Connatix Adapter Params", | ||
"description": "A schema which validates params accepted by the Connatix adapter", | ||
"type": "object", | ||
"properties": { | ||
"placementId": { | ||
"type": "string", | ||
"minLength": 1, | ||
"description": "Placement ID" | ||
}, | ||
"viewabilityPercentage": { | ||
"type": "number", | ||
"description": "Declared viewability percentage (values from 0 to 1, where 1 = 100%)", | ||
"minimum": 0, | ||
"maximum": 1 | ||
} | ||
}, | ||
"required": ["placementId"] | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please do not mix up constants and dependencies, place constants first