Skip to content
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

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
262 changes: 262 additions & 0 deletions src/main/java/org/prebid/server/bidder/connatix/ConnatixBidder.java
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";
Copy link
Collaborator

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


private final CurrencyConversionService currencyConversionService;

public ConnatixBidder(String endpointUrl,
CurrencyConversionService currencyConversionService,
JacksonMapper mapper) {
this.endpointUrl = HttpUtil.validateUrl(Objects.requireNonNull(endpointUrl));
this.currencyConversionService = currencyConversionService;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd move conversion of the bid floor inside the modifyImp method

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);
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
Copy link
Collaborator

Choose a reason for hiding this comment

The 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())) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CollectionUtils.isNotEmpty

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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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()))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BidderBid.of(bid, getBidType(bid), BID_CURRENCY))

.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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove comments

private String buildDisplayManagerVersion(BidRequest request) {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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)
: "";
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
}
Copy link
Collaborator

Choose a reason for hiding this comment

The 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);

Copy link
Collaborator

Choose a reason for hiding this comment

The 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 {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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;
Copy link
Collaborator

Choose a reason for hiding this comment

The 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();
}
}
25 changes: 25 additions & 0 deletions src/main/resources/bidder-config/connatix.yaml
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
Copy link
Collaborator

Choose a reason for hiding this comment

The 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'
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

'[UID]'

supportCors: false
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

support-cors

Copy link
Collaborator

Choose a reason for hiding this comment

The 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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

support-cors:


21 changes: 21 additions & 0 deletions src/main/resources/static/bidder-params/connatix.json
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"]
}

Loading
Loading