Repository navigation
Yahoo Ads: Use bid.mtype and fix bid type on multi-format impressions #4669
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
base: master
Are you sure you want to change the base?
Changes from all commits
f02d8e5
988dafc
5216d7e
1a498b4
f903958
69dc8e8
5ccd56f
cedfcf8
d430330
91c2d35
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 |
|---|---|---|
|
|
@@ -17,6 +17,7 @@ | |
| import io.vertx.core.MultiMap; | ||
| import io.vertx.core.http.HttpMethod; | ||
| import org.apache.commons.collections4.CollectionUtils; | ||
| import org.apache.commons.lang3.ObjectUtils; | ||
| import org.apache.commons.lang3.StringUtils; | ||
| import org.prebid.server.bidder.Bidder; | ||
| import org.prebid.server.bidder.model.BidderBid; | ||
|
|
@@ -38,6 +39,7 @@ | |
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| public class YahooAdsBidder implements Bidder<BidRequest> { | ||
|
|
||
|
|
@@ -49,6 +51,9 @@ public class YahooAdsBidder implements Bidder<BidRequest> { | |
| private static final String GPP_PROPERTY = "gpp"; | ||
| private static final String GPP_SID_PROPERTY = "gpp_sid"; | ||
|
|
||
| // VAST_MARKUP is a heuristic to find video bids without mtype on banner+video imps. | ||
| private static final Pattern VAST_MARKUP = Pattern.compile("<VAST[ \\t\\r\\n/>]"); | ||
|
|
||
| private final String endpointUrl; | ||
| private final JacksonMapper mapper; | ||
|
|
||
|
|
@@ -248,50 +253,78 @@ private static MultiMap makeHeaders(Device device) { | |
| public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) { | ||
| try { | ||
| final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class); | ||
| return Result.of(extractBids(bidResponse, httpCall.getRequest().getPayload()), Collections.emptyList()); | ||
| final List<BidderError> errors = new ArrayList<>(); | ||
| final List<BidderBid> bids = extractBids(bidResponse, httpCall.getRequest().getPayload(), errors); | ||
| return Result.of(bids, errors); | ||
| } catch (DecodeException | PreBidException e) { | ||
| return Result.withError(BidderError.badServerResponse(e.getMessage())); | ||
| } | ||
| } | ||
|
|
||
| private static List<BidderBid> extractBids(BidResponse bidResponse, BidRequest bidRequest) { | ||
| private static List<BidderBid> extractBids(BidResponse bidResponse, | ||
| BidRequest bidRequest, | ||
| List<BidderError> errors) { | ||
| final List<SeatBid> seatBids = bidResponse != null ? bidResponse.getSeatbid() : null; | ||
| if (seatBids == null) { | ||
| return Collections.emptyList(); | ||
| } | ||
| return bidsFromResponse(bidResponse, bidRequest.getImp()); | ||
| return bidsFromResponse(bidResponse, bidRequest.getImp(), errors); | ||
| } | ||
|
|
||
| private static List<BidderBid> bidsFromResponse(BidResponse bidResponse, List<Imp> imps) { | ||
| private static List<BidderBid> bidsFromResponse(BidResponse bidResponse, | ||
| List<Imp> imps, | ||
| List<BidderError> errors) { | ||
| return bidResponse.getSeatbid().stream() | ||
| .filter(Objects::nonNull) | ||
| .map(SeatBid::getBid) | ||
| .filter(Objects::nonNull) | ||
| .flatMap(Collection::stream) | ||
| .filter(Objects::nonNull) | ||
| .map(bid -> makeBidderBid(bid, imps, bidResponse.getCur())) | ||
| .map(bid -> makeBidderBid(bid, imps, bidResponse.getCur(), errors)) | ||
| .filter(Objects::nonNull) | ||
| .toList(); | ||
| } | ||
|
|
||
| private static BidderBid makeBidderBid(Bid bid, List<Imp> imps, String currency) { | ||
| final BidType bidType = getBidType(bid, imps); | ||
| return bidType != null | ||
| ? BidderBid.of(bid, bidType, currency) | ||
| : null; | ||
| private static BidderBid makeBidderBid(Bid bid, List<Imp> imps, String currency, List<BidderError> errors) { | ||
| final Imp imp = findImp(bid, imps); | ||
| try { | ||
| final BidType bidType = resolveBidType(bid, imp); | ||
| return bidType != null | ||
| ? BidderBid.of(bid, bidType, currency) | ||
| : null; | ||
| } catch (PreBidException e) { | ||
| errors.add(BidderError.badServerResponse(e.getMessage())); | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| private static BidType getBidType(Bid bid, List<Imp> imps) { | ||
| private static Imp findImp(Bid bid, List<Imp> imps) { | ||
| for (Imp imp : imps) { | ||
| if (imp.getId().equals(bid.getImpid())) { | ||
| if (imp.getBanner() != null) { | ||
| return BidType.banner; | ||
| } else if (imp.getVideo() != null) { | ||
| return BidType.video; | ||
| } | ||
| return null; | ||
| return imp; | ||
| } | ||
| } | ||
| throw new PreBidException("Unknown ad unit code '%s'".formatted(bid.getImpid())); | ||
|
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 already happened before this PR: an unknown
Contributor
Author
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. Agreed, will handle this in a separate PR. |
||
| } | ||
|
|
||
| private static BidType resolveBidType(Bid bid, Imp imp) { | ||
| return switch (ObjectUtils.getIfNull(bid.getMtype(), 0)) { | ||
| case 0 -> resolveBidTypeFromImp(bid, imp); | ||
| case 1 -> BidType.banner; | ||
| case 2 -> BidType.video; | ||
| default -> throw new PreBidException( | ||
| "Unsupported mtype %d for bid '%s'".formatted(bid.getMtype(), bid.getId())); | ||
| }; | ||
| } | ||
|
|
||
| private static BidType resolveBidTypeFromImp(Bid bid, Imp imp) { | ||
| if (imp.getVideo() != null && (imp.getBanner() == null || isVast(bid.getAdm()))) { | ||
| return BidType.video; | ||
| } | ||
| return imp.getBanner() != null ? BidType.banner : null; | ||
| } | ||
|
|
||
| private static boolean isVast(String adm) { | ||
| return StringUtils.isNotEmpty(adm) && VAST_MARKUP.matcher(adm).find(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -390,6 +390,213 @@ public void makeBidsShouldSkipNotSupportedImpAndReturnVideoBidWhenVideoPresent() | |
| .containsOnly(BidderBid.of(Bid.builder().impid("321").build(), video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnVideoBidWhenMtypeIsVideoOnMultiFormatImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").mtype(2).adm("<VAST version=\"4.0\"></VAST>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenMtypeIsBannerOnMultiFormatImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").mtype(1).adm("<div>ad</div>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldTreatZeroMtypeAsAbsent() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").mtype(0).adm("<VAST version=\"4.0\"></VAST>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnErrorForUnsupportedMtypeAndKeepOtherBids() throws JsonProcessingException { | ||
|
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. The description lists "on a single-format impression, an audio or native
Contributor
Author
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. Thanks, added a banner-only test: mtype 1 is kept as banner and mtype 4 is dropped. Go now checks the same pair, so both PRs match. |
||
| // given | ||
| final Bid audioBid = Bid.builder().id("audioBidId").impid("321").mtype(3).build(); | ||
| final Bid nativeBid = Bid.builder().id("nativeBidId").impid("321").mtype(4).build(); | ||
| final Bid videoBid = Bid.builder().id("videoBidId").impid("321").mtype(2).build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), audioBid, nativeBid, videoBid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).containsExactly( | ||
| BidderError.badServerResponse("Unsupported mtype 3 for bid 'audioBidId'"), | ||
| BidderError.badServerResponse("Unsupported mtype 4 for bid 'nativeBidId'")); | ||
| assertThat(result.getValue()).containsExactly(BidderBid.of(videoBid, video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnErrorForUnsupportedMtypeOnBannerOnlyImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bannerBid = Bid.builder().id("bannerBidId").impid("321").mtype(1).adm("<div>ad</div>").build(); | ||
| final Bid nativeBid = Bid.builder().id("nativeBidId").impid("321").mtype(4).build(); | ||
| final Imp bannerOnlyImp = Imp.builder().id("321").banner(Banner.builder().build()).build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(bannerOnlyImp, bannerBid, nativeBid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()) | ||
| .containsExactly(BidderError.badServerResponse("Unsupported mtype 4 for bid 'nativeBidId'")); | ||
| assertThat(result.getValue()).containsExactly(BidderBid.of(bannerBid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnVideoBidWhenNoMtypeAndAdmIsVastOnMultiFormatImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<VAST version=\"4.0\"></VAST>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenNoMtypeAndAdmIsHtmlOnMultiFormatImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<div>ad</div>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnVideoBidWhenVastFollowsXmlDeclaration() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder() | ||
| .impid("321") | ||
| .adm("<?xml version=\"1.0\" ?><VAST version=\"2.0\"><Ad></Ad></VAST>") | ||
| .build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnVideoBidWhenVastAttributesStartOnNextLine() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<VAST\n version=\"4.0\"></VAST>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, video, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenVastTagIsLowercase() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<vast version=\"4.0\"></vast>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenAdmHasNoVastElement() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<VASTAdTagURI>u</VASTAdTagURI>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenAdmMentionsVastOnlyAsText() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<div>VAST</div>").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenNoMtypeAndNoAdmOnMultiFormatImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(givenMultiFormatImp(), bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeBidsShouldReturnBannerBidWhenNoMtypeOnBannerOnlyImp() throws JsonProcessingException { | ||
| // given | ||
| final Bid bid = Bid.builder().impid("321").adm("<div>ad</div>").build(); | ||
| final Imp bannerOnlyImp = Imp.builder().id("321").banner(Banner.builder().build()).build(); | ||
| final BidderCall<BidRequest> httpCall = givenHttpCall(bannerOnlyImp, bid); | ||
|
|
||
| // when | ||
| final Result<List<BidderBid>> result = target.makeBids(httpCall, null); | ||
|
|
||
| // then | ||
| assertThat(result.getErrors()).isEmpty(); | ||
| assertThat(result.getValue()).containsOnly(BidderBid.of(bid, banner, "USD")); | ||
| } | ||
|
|
||
| @Test | ||
| public void makeHttpRequestsShouldPreserveTopLevel26RegsAndExtTypedFields() { | ||
| // given | ||
|
|
@@ -955,10 +1162,27 @@ private static BidResponse givenBidResponse(Function<Bid.BidBuilder, Bid.BidBuil | |
| .build(); | ||
| } | ||
|
|
||
| private static Imp givenMultiFormatImp() { | ||
| return Imp.builder() | ||
| .id("321") | ||
| .banner(Banner.builder().build()) | ||
| .video(Video.builder().build()) | ||
| .build(); | ||
| } | ||
|
|
||
| private static BidderCall<BidRequest> givenHttpCall(BidRequest bidRequest, String body) { | ||
| return BidderCall.succeededHttp( | ||
| HttpRequest.<BidRequest>builder().payload(bidRequest).build(), | ||
| HttpResponse.of(200, null, body), | ||
| null); | ||
| } | ||
|
|
||
| private static BidderCall<BidRequest> givenHttpCall(Imp imp, Bid... bids) throws JsonProcessingException { | ||
| return givenHttpCall( | ||
| BidRequest.builder().imp(singletonList(imp)).build(), | ||
| mapper.writeValueAsString(BidResponse.builder() | ||
| .cur("USD") | ||
| .seatbid(singletonList(SeatBid.builder().bid(asList(bids)).build())) | ||
| .build())); | ||
| } | ||
| } | ||
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.
Minor:
find()looks anywhere inadm, so an HTML banner whose script embeds a VAST string (some outstream renderers do this) would be typed asvideoon a multi-format impression that has nomtype. That's an acceptable trade-off for the fallback path. A short comment saying it's a heuristic for responses withoutmtypewould help the next reader.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.
Thanks, added a comment.