Skip to content

Commit

Permalink
Appnexus: make AdPodId optional (#1397)
Browse files Browse the repository at this point in the history
  • Loading branch information
And1sS authored and nickluck8 committed Aug 10, 2021
1 parent b8e6c4b commit 61ad940
Show file tree
Hide file tree
Showing 7 changed files with 242 additions and 165 deletions.
117 changes: 60 additions & 57 deletions src/main/java/org/prebid/server/bidder/appnexus/AppnexusBidder.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
Expand All @@ -11,10 +12,12 @@
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.http.HttpMethod;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.model.Endpoint;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.appnexus.model.ImpWithMemberId;
import org.prebid.server.bidder.appnexus.model.ImpWithExtProperties;
import org.prebid.server.bidder.appnexus.proto.AppnexusBidExt;
import org.prebid.server.bidder.appnexus.proto.AppnexusBidExtAppnexus;
import org.prebid.server.bidder.appnexus.proto.AppnexusImpExt;
Expand Down Expand Up @@ -182,47 +185,33 @@ public AppnexusBidder(String endpointUrl, JacksonMapper mapper) {
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest bidRequest) {
final List<BidderError> errors = new ArrayList<>();
final String defaultDisplayManagerVer = makeDefaultDisplayManagerVer(bidRequest);

final List<Imp> processedImps = new ArrayList<>();
final Set<String> memberIds = new HashSet<>();
final Set<String> uniqueIds = new HashSet<>();
Boolean generateAdPodId = null;

for (final Imp imp : bidRequest.getImp()) {
try {
final ImpWithMemberId impWithMemberId = makeImpWithMemberId(imp, defaultDisplayManagerVer);
processedImps.add(impWithMemberId.getImp());
memberIds.add(impWithMemberId.getMemberId());
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
}
final ImpWithExtProperties impWithExtProperties = processImp(imp, defaultDisplayManagerVer);
final Boolean impGenerateAdPodId = impWithExtProperties.getGenerateAdPodId();

final Set<String> uniqueIds = memberIds.stream()
.filter(Objects::nonNull)
.collect(Collectors.toSet());
generateAdPodId = ObjectUtils.defaultIfNull(generateAdPodId, impGenerateAdPodId);
if (!Objects.equals(generateAdPodId, impGenerateAdPodId)) {
return Result.withError(BidderError.badInput(
"Generate ad pod option should be same for all pods in request"));
}

final String url;
if (CollectionUtils.isNotEmpty(uniqueIds)) {
url = String.format("%s?member_id=%s", endpointUrl, uniqueIds.iterator().next());
try {
validateMemberId(uniqueIds);
processedImps.add(impWithExtProperties.getImp());
final String memberId = impWithExtProperties.getMemberId();
if (memberId != null) {
uniqueIds.add(memberId);
}
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
} else {
url = endpointUrl;
}

final List<HttpRequest<BidRequest>> httpRequests;
if (isVideoRequest(bidRequest)) {
httpRequests = groupImpsByPod(processedImps)
.values().stream()
.map(podImps -> splitHttpRequests(bidRequest, updateRequestExtForVideo(bidRequest), podImps, url))
.flatMap(Collection::stream)
.collect(Collectors.toList());
} else {
httpRequests = splitHttpRequests(bidRequest, updateRequestExt(bidRequest), processedImps, url);
}

return Result.of(httpRequests, errors);
final String url = constructUrl(uniqueIds, errors);
return Result.of(constructRequests(bidRequest, processedImps, url, generateAdPodId), errors);
}

private String makeDefaultDisplayManagerVer(BidRequest bidRequest) {
Expand Down Expand Up @@ -290,44 +279,57 @@ private static boolean isIncludeBrandCategory(ExtRequest extRequest) {
return includebrandcategory != null;
}

private String constructUrl(Set<String> ids, List<BidderError> errors) {
if (CollectionUtils.isNotEmpty(ids)) {
final String url = String.format("%s?member_id=%s", endpointUrl, ids.iterator().next());
try {
validateMemberId(ids);
} catch (PreBidException e) {
errors.add(BidderError.badInput(e.getMessage()));
}
return url;
}
return endpointUrl;
}

private List<HttpRequest<BidRequest>> constructRequests(BidRequest bidRequest,
List<Imp> imps,
String url,
Boolean generateAdPodId) {
if (isVideoRequest(bidRequest) && BooleanUtils.isTrue(generateAdPodId)) {
return groupImpsByPod(imps)
.values().stream()
.map(podImps -> splitHttpRequests(bidRequest, updateRequestExtForVideo(bidRequest), podImps, url))
.flatMap(Collection::stream)
.collect(Collectors.toList());
} else {
return splitHttpRequests(bidRequest, updateRequestExt(bidRequest), imps, url);
}
}

private Map<String, List<Imp>> groupImpsByPod(List<Imp> processedImps) {
return processedImps.stream()
.collect(Collectors.groupingBy(imp -> StringUtils.substringBefore(imp.getId(), POD_SEPARATOR)));
}

private List<HttpRequest<BidRequest>> splitHttpRequests(BidRequest bidRequest,
ExtRequest requestExt,
List<Imp> processedImps,
List<Imp> imps,
String url) {
final List<HttpRequest<BidRequest>> result = Lists.partition(imps, MAX_IMP_PER_REQUEST)
.stream()
.map(impsChunk -> createHttpRequest(bidRequest, requestExt, impsChunk, url))
.collect(Collectors.toList());

// Let's say there are 35 impressions and limit impressions per request equals to 10.
// In this case we need to create 4 requests with 10, 10, 10 and 5 impressions.
// With this formula initial capacity=(35+10-1)/10 = 4
final int impSize = processedImps.size();
final int numberOfRequests = (impSize + MAX_IMP_PER_REQUEST - 1) / MAX_IMP_PER_REQUEST;
final List<HttpRequest<BidRequest>> spitedRequests = new ArrayList<>(numberOfRequests);

int startIndex = 0;
boolean impsLeft = true;
while (impsLeft) {
int endIndex = startIndex + MAX_IMP_PER_REQUEST;
if (endIndex >= impSize) {
impsLeft = false;
endIndex = impSize;
}
spitedRequests.add(
createHttpRequest(bidRequest, requestExt, processedImps.subList(startIndex, endIndex), url));
startIndex = endIndex;
}

return spitedRequests;
return result.isEmpty()
? Collections.singletonList(createHttpRequest(bidRequest, requestExt, imps, url))
: result;
}

private HttpRequest<BidRequest> createHttpRequest(BidRequest bidRequest,
ExtRequest requestExt,
List<Imp> imps,
String url) {

final BidRequest outgoingRequest = bidRequest.toBuilder()
.imp(imps)
.ext(requestExt)
Expand All @@ -342,7 +344,7 @@ private HttpRequest<BidRequest> createHttpRequest(BidRequest bidRequest,
.build();
}

private ImpWithMemberId makeImpWithMemberId(Imp imp, String defaultDisplayManagerVer) {
private ImpWithExtProperties processImp(Imp imp, String defaultDisplayManagerVer) {
if (imp.getAudio() != null) {
throw new PreBidException(
String.format("Appnexus doesn't support audio Imps. Ignoring Imp ID=%s", imp.getId()));
Expand All @@ -369,7 +371,8 @@ private ImpWithMemberId makeImpWithMemberId(Imp imp, String defaultDisplayManage
impBuilder.displaymanagerver(defaultDisplayManagerVer);
}

return ImpWithMemberId.of(impBuilder.build(), appnexusExt.getMember());
return ImpWithExtProperties.of(impBuilder.build(), appnexusExt.getMember(),
appnexusExt.getGenerateAdPodId());
}

private static boolean bidFloorIsValid(BigDecimal bidFloor) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@

@AllArgsConstructor(staticName = "of")
@Value
public class ImpWithMemberId {
public class ImpWithExtProperties {

Imp imp;

String memberId;

Boolean generateAdPodId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,7 @@ public class ExtImpAppnexus {

Boolean usePmtRule;

Boolean generateAdPodId;

ObjectNode privateSizes; // At this time we do no processing on the private sizes, so just leaving it as a JSON blob
}
4 changes: 4 additions & 0 deletions src/main/resources/static/bidder-params/appnexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@
"type": "boolean",
"description": "Boolean to signal AppNexus to apply the relevant payment rule"
},
"generate_ad_pod_id": {
"type": "boolean",
"description": "Boolean to signal AppNexus to add ad pod id to each request"
},
"private_sizes": {
"type": "array",
"items": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -790,7 +791,7 @@ public void makeHttpRequestShouldReturnSingleRequestWhenOnePod() {
imp -> imp
.id(String.format("1_%d", impIdSuffix))
.banner(Banner.builder().build()),
ext -> ext.placementId(10)))
ext -> ext.placementId(10).generateAdPodId(true)))
.collect(Collectors.toList());
final BidRequest bidRequest = BidRequest.builder()
.imp(imps)
Expand Down Expand Up @@ -820,7 +821,7 @@ public void makeHttpRequestShouldReturnMultipleRequestsWhenOnePodAndManyImps() {
imp -> imp
.id(String.format("1_%d", impIdSuffix))
.banner(Banner.builder().build()),
ext -> ext.placementId(10)))
ext -> ext.placementId(10).generateAdPodId(true)))
.collect(Collectors.toList());
final BidRequest bidRequest = BidRequest.builder()
.imp(imps)
Expand Down Expand Up @@ -852,7 +853,7 @@ public void makeHttpRequestShouldReturnMultipleRequestsWhenTwoPods() {
imp -> imp
.id(String.format("%d_%d", impIdPrefix, impIdSuffix))
.banner(Banner.builder().build()),
ext -> ext.placementId(10))))
ext -> ext.placementId(10).generateAdPodId(true))))
.collect(Collectors.toList());
final BidRequest bidRequest = BidRequest.builder()
.imp(imps)
Expand Down Expand Up @@ -884,7 +885,7 @@ public void makeHttpRequestShouldReturnMultipleRequestsWhenTwoPodsAndManyImps()
imp -> imp
.id(String.format("%d_%d", impIdPrefix, impIdSuffix))
.banner(Banner.builder().build()),
ext -> ext.placementId(10))))
ext -> ext.placementId(10).generateAdPodId(true))))
.collect(Collectors.toList());
final BidRequest bidRequest = BidRequest.builder()
.imp(imps)
Expand All @@ -906,6 +907,70 @@ public void makeHttpRequestShouldReturnMultipleRequestsWhenTwoPodsAndManyImps()
.matches(adPodIds -> new HashSet<>(adPodIds).size() == 2); // adPodIds should be different
}

@Test
public void makeHttpRequestShouldReturnErrorIfRequestContainsMultipleGenerateAdPodIdsValues() {
// given
final List<Imp> imps = IntStream.rangeClosed(1, 2)
.boxed()
.flatMap(impIdPrefix -> IntStream.rangeClosed(0, 15)
.mapToObj(impIdSuffix -> givenImp(
imp -> imp
.id(String.format("%d_%d", impIdPrefix, impIdSuffix))
.banner(Banner.builder().build()),
ext -> ext.placementId(10).generateAdPodId(impIdSuffix % 2 == 0))))
.collect(Collectors.toList());
final BidRequest bidRequest = BidRequest.builder()
.imp(imps)
.ext(ExtRequest.of(ExtRequestPrebid.builder()
.pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
.build()))
.build();

// when
final Result<List<HttpRequest<BidRequest>>> result = appnexusBidder.makeHttpRequests(bidRequest);

// then
assertThat(result.getValue()).isEmpty();
assertThat(result.getErrors())
.containsExactly(BidderError.badInput("Generate ad pod option should be same for all pods in request"));
}

@Test
public void makeHttpRequestShouldNotGenerateAdPodIdWhenFlagIsNotSetInRequestImpExt() {
// given
final List<Imp> imps = IntStream.rangeClosed(1, 2)
.boxed()
.flatMap(impIdPrefix -> IntStream.rangeClosed(0, 15)
.mapToObj(impIdSuffix -> givenImp(
imp -> imp
.id(String.format("%d_%d", impIdPrefix, impIdSuffix))
.banner(Banner.builder().build()),
ext -> ext.placementId(10).generateAdPodId(false))))
.collect(Collectors.toList());
final BidRequest bidRequest = BidRequest.builder()
.imp(imps)
.ext(ExtRequest.of(ExtRequestPrebid.builder()
.targeting(ExtRequestTargeting.builder()
.includebrandcategory(ExtIncludeBrandCategory.of(null, null, null))
.build())
.pbs(ExtRequestPrebidPbs.of(Endpoint.openrtb2_video.value()))
.build()))
.build();

// when
final Result<List<HttpRequest<BidRequest>>> result = appnexusBidder.makeHttpRequests(bidRequest);

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue())
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> mapper.convertValue(ext.getProperties(), AppnexusReqExt.class))
.extracting(AppnexusReqExt::getAppnexus).doesNotContainNull()
.extracting(AppnexusReqExtAppnexus::getAdpodId)
.matches(adPodIds -> adPodIds.stream().allMatch(Objects::isNull));
}

@Test
public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() {
// given
Expand Down Expand Up @@ -1139,7 +1204,7 @@ private static BidRequest givenBidRequest(UnaryOperator<BidRequestBuilder> bidRe
UnaryOperator<ImpBuilder> impCustomizer,
UnaryOperator<ExtImpAppnexusBuilder> extCustomizer) {
return bidRequestCustomizer.apply(BidRequest.builder()
.imp(singletonList(givenImp(impCustomizer, extCustomizer))))
.imp(singletonList(givenImp(impCustomizer, extCustomizer))))
.build();
}

Expand All @@ -1150,7 +1215,7 @@ private static BidRequest givenBidRequest(UnaryOperator<ImpBuilder> impCustomize
private static Imp givenImp(UnaryOperator<ImpBuilder> impCustomizer,
UnaryOperator<ExtImpAppnexusBuilder> extCustomizer) {
return impCustomizer.apply(Imp.builder()
.ext(givenExt(extCustomizer)))
.ext(givenExt(extCustomizer)))
.build();
}

Expand Down Expand Up @@ -1184,15 +1249,16 @@ private static String givenBidResponse(
UnaryOperator<AppnexusBidExtAppnexus.AppnexusBidExtAppnexusBuilder> bidExtCustomizer)
throws JsonProcessingException {

return mapper.writeValueAsString(bidResponseCustomizer.apply(BidResponse.builder()
.seatbid(singletonList(SeatBid.builder()
.bid(singletonList(bidCustomizer.apply(Bid.builder()
.impid("impId")
.ext(mapper.valueToTree(AppnexusBidExt.of(
bidExtCustomizer.apply(AppnexusBidExtAppnexus.builder().bidAdType(BANNER_TYPE))
.build()))))
.build()))
.build())))
return mapper.writeValueAsString(bidResponseCustomizer.apply(
BidResponse.builder()
.seatbid(singletonList(SeatBid.builder()
.bid(singletonList(bidCustomizer.apply(Bid.builder()
.impid("impId")
.ext(mapper.valueToTree(AppnexusBidExt.of(bidExtCustomizer.apply(
AppnexusBidExtAppnexus.builder()
.bidAdType(BANNER_TYPE))
.build())))).build()))
.build())))
.build());
}

Expand Down
Loading

0 comments on commit 61ad940

Please sign in to comment.