diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java index 61c137a78df..62921443bc5 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -11,6 +11,9 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import io.netty.handler.codec.http.HttpHeaderValues; @@ -34,11 +37,12 @@ import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.proto.openrtb.ext.ExtPrebid; +import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; -import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRequest; import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.yieldlab.ExtImpYieldlab; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -107,6 +111,7 @@ public Result>> makeHttpRequests(BidRequest request) { return Result.withValue(HttpRequest.builder() .method(HttpMethod.GET) .uri(uri) + .impIds(request.getImp().stream().map(Imp::getId).collect(Collectors.toSet())) .headers(resolveHeaders(request.getSite(), request.getDevice(), request.getUser())) .build()); } @@ -149,6 +154,10 @@ private ExtImpYieldlab parseImpExt(Imp imp) { } } + private static String encodeValue(String value) { + return value == null ? StringUtils.EMPTY : HttpUtil.encodeUrl(value); + } + private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { // for passing validation tests final String timestamp = isDebugEnabled(request) ? "200000" : String.valueOf(clock.instant().getEpochSecond()); @@ -168,7 +177,7 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { .addParameter("ts", timestamp) .addParameter("t", getTargetingValues(extImpYieldlab)); - final String formats = makeFormats(request, extImpYieldlab); + final String formats = makeFormats(request); if (formats != null) { uriBuilder.addParameter("sizes", formats); @@ -212,20 +221,37 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { uriBuilder.addParameter("gdpr_consent", consent); } + Optional.ofNullable(request.getSource()) + .map(Source::getExt) + .map(ExtSource::getSchain) + .map(this::resolveSupplyChain) + .ifPresent(sChain -> uriBuilder.addParameter("schain", sChain)); + extractDsaRequestParamsFromBidRequest(request).forEach(uriBuilder::addParameter); return uriBuilder.toString(); } - private String makeFormats(BidRequest request, ExtImpYieldlab extImp) { + private String makeFormats(BidRequest request) { final List formats = new ArrayList<>(); for (Imp imp: request.getImp()) { - if (isBanner(imp)) { - Stream.ofNullable(imp.getBanner().getFormat()) - .flatMap(Collection::stream) - .map(format -> "%s:%d|%d".formatted(extImp.getAdslotId(), format.getW(), format.getH())) - .forEach(formats::add); + if (!isBanner(imp)) { + continue; + + } + final ExtImpYieldlab extImp = parseImpExt(imp); + if (extImp == null) { + continue; } + + final List formatsPerAdSlot = new ArrayList<>(); + Stream.ofNullable(imp.getBanner().getFormat()) + .flatMap(Collection::stream) + .map(format -> "%dx%d".formatted(format.getW(), format.getH())) + .forEach(formatsPerAdSlot::add); + + final String formatsPerAdSlotString = String.join("|", formatsPerAdSlot); + formats.add("%s:%s".formatted(extImp.getAdslotId(), formatsPerAdSlotString)); } return formats.isEmpty() ? null : String.join(",", formats); @@ -274,6 +300,33 @@ private static String getConsentParameter(User user) { return ObjectUtils.defaultIfNull(consent, ""); } + private String resolveSupplyChain(SupplyChain schain) { + final List nodes = schain.getNodes(); + if (CollectionUtils.isEmpty(nodes)) { + return StringUtils.EMPTY; + } + + final String sChainPrefix = "%s,%d".formatted(schain.getVer(), schain.getComplete()); + + final StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(sChainPrefix); + + for (SupplyChainNode node : schain.getNodes()) { + stringBuilder.append("!%s,%s,%s,%s,%s,%s,%s".formatted( + encodeValue(node.getAsi()), + encodeValue(node.getSid()), + node.getHp() == null ? StringUtils.EMPTY : node.getHp(), + encodeValue(node.getRid()), + encodeValue(node.getName()), + encodeValue(node.getDomain()), + node.getExt() == null + ? StringUtils.EMPTY + : HttpUtil.encodeUrl(mapper.encodeToString(node.getExt())))); + } + + return stringBuilder.toString(); + } + private static Map extractDsaRequestParamsFromBidRequest(BidRequest request) { return Optional.ofNullable(request.getRegs()) .map(Regs::getExt) @@ -360,11 +413,12 @@ public Result> makeBids(BidderCall httpCall, BidRequest bi return Result.withError(BidderError.badServerResponse(e.getMessage())); } + final List extImpYieldlabs = collectImpExt(bidRequest.getImp()); final List bidderBids = new ArrayList<>(); for (int i = 0; i < yieldlabResponses.size(); i++) { final BidderBid bidderBid; try { - bidderBid = resolveBidderBid(yieldlabResponses, i, bidRequest); + bidderBid = resolveBidderBid(yieldlabResponses, i, bidRequest, extImpYieldlabs); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -377,10 +431,13 @@ public Result> makeBids(BidderCall httpCall, BidRequest bi } private BidderBid resolveBidderBid(List yieldlabResponses, - int currentImpIndex, BidRequest bidRequest) { + int currentImpIndex, + BidRequest bidRequest, + List extImpYieldlabs) { + final YieldlabResponse yieldlabResponse = yieldlabResponses.get(currentImpIndex); - final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); + final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), extImpYieldlabs); if (matchedExtImp == null) { throw new PreBidException("Invalid extension"); } @@ -403,7 +460,7 @@ private BidderBid resolveBidderBid(List yieldlabResponses, return null; } - addBidParams(yieldlabResponse, bidRequest, updatedBid) + addBidParams(yieldlabResponse, bidRequest, updatedBid, extImpYieldlabs) .impid(currentImp.getId()); return BidderBid.of(updatedBid.build(), bidType, BID_CURRENCY); @@ -419,16 +476,19 @@ private List decodeBodyToBidList(BidderCall httpCall) { } } - private ExtImpYieldlab getMatchedExtImp(Integer responseId, List imps) { - return collectImpExt(imps).stream() + private ExtImpYieldlab getMatchedExtImp(Integer responseId, List extImpYieldlabs) { + return extImpYieldlabs.stream() .filter(ext -> ext.getAdslotId().equals(String.valueOf(responseId))) .findFirst() .orElse(null); } - private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, BidRequest bidRequest, - Bid.BidBuilder updatedBid) { - final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); + private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, + BidRequest bidRequest, + Bid.BidBuilder updatedBid, + List extImpYieldlabs) { + + final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), extImpYieldlabs); if (matchedExtImp == null) { throw new PreBidException("Invalid extension"); diff --git a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java index 2c3a5e17a67..a1773f44e52 100644 --- a/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/yieldlab/YieldlabBidderTest.java @@ -11,6 +11,9 @@ import com.iab.openrtb.request.Imp; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.Source; +import com.iab.openrtb.request.SupplyChain; +import com.iab.openrtb.request.SupplyChainNode; import com.iab.openrtb.request.User; import com.iab.openrtb.request.Video; import com.iab.openrtb.response.Bid; @@ -29,6 +32,7 @@ import org.prebid.server.proto.openrtb.ext.request.DsaTransparency; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtRegsDsa; +import org.prebid.server.proto.openrtb.ext.request.ExtSource; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.proto.openrtb.ext.request.yieldlab.ExtImpYieldlab; import org.prebid.server.proto.openrtb.ext.response.BidType; @@ -118,6 +122,18 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { .regs(Regs.builder().coppa(1).ext(ExtRegs.of(1, "usPrivacy", null, null)).build()) .user(User.builder().buyeruid("buyeruid").ext(ExtUser.builder().consent("consent").build()).build()) .site(Site.builder().page("http://www.example.com").build()) + .source(Source.builder().ext(ExtSource.of(SupplyChain.of(1, List.of( + SupplyChainNode.of( + "exchange1.com", + "1234!abcd", + "bid request&%1", + "publisher", + "publisher.com", + 1, + mapper.createObjectNode() + .put("freeFormData", 1) + .set("nested", mapper.createObjectNode().put("isTrue", true))) + ), "1.0", null))).build()) .build(); // when @@ -131,8 +147,11 @@ public void makeHttpRequestsShouldSendRequestToModifiedUrlWithHeaders() { .extracting(HttpRequest::getUri) .allSatisfy(uri -> { assertThat(uri).startsWith("https://test.endpoint.com/1?content=json&pvid=true&ts="); - assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&sizes=1%3A1%7C1%2C1%3A2%7C2&" - + "ids=buyeruid&yl_rtb_ifa&yl_rtb_devicetype=1&gdpr=1&gdpr_consent=consent"); + assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&sizes=1%3A1x1%7C2x2&" + + "ids=buyeruid&yl_rtb_ifa&yl_rtb_devicetype=1&gdpr=1&gdpr_consent=consent&" + + "schain=1.0%2C1%21exchange1.com%2C1234%2521abcd%2C1%2Cbid%2Brequest%2526%25251%2C" + + "publisher%2Cpublisher.com%2C%257B%2522freeFormData%2522%253A1%252C%2522" + + "nested%2522%253A%257B%2522isTrue%2522%253Atrue%257D%257D"); final String ts = uri.substring(54, uri.indexOf("&t=")); assertThat(Long.parseLong(ts)).isEqualTo(expectedTime); }); @@ -192,7 +211,7 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { .extracting(HttpRequest::getUri) .allSatisfy(uri -> { assertThat(uri).startsWith("https://test.endpoint.com/1,2?content=json&pvid=true&ts="); - assertThat(uri).endsWith("&t=key1%3Dvalue1&ids=buyeruid&yl_rtb_ifa&" + assertThat(uri).endsWith("&t=key1%3Dvalue1&sizes=1%3A%2C2%3A&ids=buyeruid&yl_rtb_ifa&" + "yl_rtb_devicetype=1&gdpr=1&gdpr_consent=consent"); }); }