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 cf99e41de84..8c06a3794fa 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/YieldlabBidder.java @@ -1,16 +1,19 @@ package org.prebid.server.bidder.yieldlab; -import com.fasterxml.jackson.core.JsonProcessingException; 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.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Geo; 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; @@ -19,6 +22,7 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.http.client.utils.URIBuilder; import org.prebid.server.bidder.Bidder; import org.prebid.server.bidder.model.BidderBid; @@ -26,22 +30,20 @@ import org.prebid.server.bidder.model.BidderError; import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.yieldlab.model.YieldlabDigitalServicesActResponse; -import org.prebid.server.bidder.yieldlab.model.YieldlabResponse; +import org.prebid.server.bidder.yieldlab.model.YieldlabBid; import org.prebid.server.exception.PreBidException; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; -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; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; +import org.prebid.server.util.BidderUtil; import org.prebid.server.util.HttpUtil; import java.math.BigDecimal; @@ -52,20 +54,23 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; -import java.util.stream.Stream; public class YieldlabBidder implements Bidder { - private static final Logger logger = LoggerFactory.getLogger(YieldlabBidder.class); private static final TypeReference> YIELDLAB_EXT_TYPE_REFERENCE = new TypeReference<>() { }; + private static final TypeReference> YIELDLAB_BID_TYPE_REFERENCE = + new TypeReference<>() { + }; + private static final String BID_CURRENCY = "EUR"; private static final String AD_SLOT_ID_SEPARATOR = ","; private static final String AD_SIZE_SEPARATOR = "x"; @@ -95,11 +100,12 @@ public YieldlabBidder(String endpointUrl, Clock clock, JacksonMapper mapper) { @Override public Result>> makeHttpRequests(BidRequest request) { - final ExtImpYieldlab modifiedExtImp = constructExtImp(request.getImp()); + final Map extImps = collectImpExt(request.getImp()); + final ExtImpYieldlab modifiedExtImp = mergeExtImps(extImps.values()); final String uri; try { - uri = makeUrl(modifiedExtImp, request); + uri = makeUrl(modifiedExtImp, request, extImps); } catch (PreBidException e) { return Result.withError(BidderError.badInput(e.getMessage())); } @@ -107,35 +113,32 @@ public Result>> makeHttpRequests(BidRequest request) { return Result.withValue(HttpRequest.builder() .method(HttpMethod.GET) .uri(uri) + .impIds(BidderUtil.impIds(request)) .headers(resolveHeaders(request.getSite(), request.getDevice(), request.getUser())) .build()); } - private ExtImpYieldlab constructExtImp(List imps) { - final List extImps = collectImpExt(imps); - - final List adSlotIds = extImps.stream() + private static ExtImpYieldlab mergeExtImps(Collection extImps) { + final String adSlotIdsParams = extImps.stream() .map(ExtImpYieldlab::getAdslotId) - .filter(Objects::nonNull) - .toList(); + .map(StringUtils::defaultString) + .collect(Collectors.joining(AD_SLOT_ID_SEPARATOR)); - final Map targeting = extImps.stream() + final Map targeting = new HashMap<>(); + extImps.stream() .map(ExtImpYieldlab::getTargeting) .filter(Objects::nonNull) - .flatMap(map -> map.entrySet().stream()) - .filter(entry -> entry.getKey() != null) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (channel1, channel2) -> channel1)); + .forEach(targeting::putAll); - final String adSlotIdsParams = adSlotIds.stream().sorted().collect(Collectors.joining(AD_SLOT_ID_SEPARATOR)); return ExtImpYieldlab.builder().adslotId(adSlotIdsParams).targeting(targeting).build(); } - private List collectImpExt(List imps) { - final List extImps = new ArrayList<>(); + private Map collectImpExt(List imps) { + final Map extImps = new HashMap<>(); for (Imp imp : imps) { final ExtImpYieldlab extImpYieldlab = parseImpExt(imp); if (extImpYieldlab != null) { - extImps.add(extImpYieldlab); + extImps.put(imp.getId(), extImpYieldlab); } } return extImps; @@ -149,10 +152,7 @@ private ExtImpYieldlab parseImpExt(Imp imp) { } } - private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { - // for passing validation tests - final String timestamp = isDebugEnabled(request) ? "200000" : String.valueOf(clock.instant().getEpochSecond()); - + private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request, Map extImps) { final String updatedPath = "%s/%s".formatted(endpointUrl, extImpYieldlab.getAdslotId()); final URIBuilder uriBuilder; @@ -165,10 +165,10 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { uriBuilder .addParameter("content", "json") .addParameter("pvid", "true") - .addParameter("ts", timestamp) + .addParameter("ts", resolveNumberParameter(clock.instant().getEpochSecond())) .addParameter("t", getTargetingValues(extImpYieldlab)); - final String formats = makeFormats(request, extImpYieldlab); + final String formats = makeFormats(request, extImps); if (formats != null) { uriBuilder.addParameter("sizes", formats); @@ -176,7 +176,7 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { final User user = request.getUser(); if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { - uriBuilder.addParameter("ids", String.join("ylid:", user.getBuyeruid())); + uriBuilder.addParameter("ids", "ylid:" + StringUtils.defaultString(user.getBuyeruid())); } final Device device = request.getDevice(); @@ -191,8 +191,8 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { final Geo geo = device.getGeo(); if (geo != null) { - uriBuilder.addParameter("lat", resolveNumberParameter(geo.getLat())); - uriBuilder.addParameter("lon", resolveNumberParameter(geo.getLon())); + uriBuilder.addParameter("lat", ObjectUtils.defaultIfNull(geo.getLat(), 0f).toString()); + uriBuilder.addParameter("lon", ObjectUtils.defaultIfNull(geo.getLon(), 0f).toString()); } } @@ -209,7 +209,12 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { final String consent = getConsentParameter(request.getUser()); if (StringUtils.isNotBlank(consent)) { - uriBuilder.addParameter("consent", consent); + uriBuilder.addParameter("gdpr_consent", consent); + } + + final String schain = getSchainParameter(request.getSource()); + if (schain != null) { + uriBuilder.addParameter("schain", schain); } extractDsaRequestParamsFromBidRequest(request).forEach(uriBuilder::addParameter); @@ -217,15 +222,22 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) { return uriBuilder.toString(); } - private String makeFormats(BidRequest request, ExtImpYieldlab extImp) { - final List formats = new ArrayList<>(); + private String makeFormats(BidRequest request, Map extImps) { + final List formats = new LinkedList<>(); 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 = extImps.get(imp.getId()); + if (extImp == null) { + continue; + } + + final String formatsPerAdSlotString = CollectionUtils.emptyIfNull(imp.getBanner().getFormat()).stream() + .map(format -> "%dx%d".formatted(format.getW(), format.getH())) + .collect(Collectors.joining("|")); + + formats.add("%s:%s".formatted(extImp.getAdslotId(), formatsPerAdSlotString)); } return formats.isEmpty() ? null : String.join(",", formats); @@ -235,19 +247,6 @@ private boolean isBanner(Imp imp) { return imp.getBanner() != null && imp.getXNative() == null && imp.getVideo() == null && imp.getAudio() == null; } - /** - * Determines debug flag from {@link BidRequest} or {@link ExtRequest}. - */ - private static boolean isDebugEnabled(BidRequest bidRequest) { - if (Objects.equals(bidRequest.getTest(), 1)) { - return true; - } - - final ExtRequest extRequest = bidRequest.getExt(); - final ExtRequestPrebid extRequestPrebid = extRequest != null ? extRequest.getPrebid() : null; - return extRequestPrebid != null && Objects.equals(extRequestPrebid.getDebug(), 1); - } - private String getTargetingValues(ExtImpYieldlab extImpYieldlab) { final URIBuilder uriBuilder = new URIBuilder(); @@ -259,19 +258,70 @@ private String getTargetingValues(ExtImpYieldlab extImpYieldlab) { } private static String getGdprParameter(Regs regs) { - if (regs != null) { - final Integer gdpr = regs.getExt() != null ? regs.getExt().getGdpr() : null; - if (gdpr != null && (gdpr == 0 || gdpr == 1)) { - return gdpr.toString(); - } - } - return ""; + return Optional.ofNullable(regs) + .map(Regs::getExt) + .map(ExtRegs::getGdpr) + .filter(gdpr -> gdpr == 0 || gdpr == 1) + .map(Object::toString) + .orElse(StringUtils.EMPTY); } private static String getConsentParameter(User user) { - final ExtUser extUser = user != null ? user.getExt() : null; - final String consent = extUser != null ? extUser.getConsent() : null; - return ObjectUtils.defaultIfNull(consent, ""); + return Optional.ofNullable(user) + .map(User::getExt) + .map(ExtUser::getConsent) + .orElse(StringUtils.EMPTY); + } + + private String getSchainParameter(Source source) { + return Optional.ofNullable(source) + .map(Source::getExt) + .map(ExtSource::getSchain) + .map(this::resolveSupplyChain) + .orElse(null); + } + + private String resolveSupplyChain(SupplyChain schain) { + final List nodes = schain.getNodes(); + if (CollectionUtils.isEmpty(nodes)) { + return null; + } + + final StringBuilder schainBuilder = new StringBuilder(); + + schainBuilder.append(schain.getVer()); + schainBuilder.append(","); + schainBuilder.append(ObjectUtils.defaultIfNull(schain.getComplete(), 0)); + for (SupplyChainNode node : schain.getNodes()) { + schainBuilder.append("!"); + schainBuilder.append(encodeValue(node.getAsi())); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getSid())); + schainBuilder.append(","); + + schainBuilder.append(node.getHp() == null ? StringUtils.EMPTY : node.getHp()); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getRid())); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getName())); + schainBuilder.append(","); + + schainBuilder.append(encodeValue(node.getDomain())); + schainBuilder.append(","); + + schainBuilder.append(node.getExt() == null + ? StringUtils.EMPTY + : HttpUtil.encodeUrl(mapper.encodeToString(node.getExt()))); + } + + return schainBuilder.toString(); + } + + private static String encodeValue(String value) { + return value == null ? StringUtils.EMPTY : HttpUtil.encodeUrl(value); } private static Map extractDsaRequestParamsFromBidRequest(BidRequest request) { @@ -310,25 +360,29 @@ private static Map extractDsaRequestParamsFromDsaRegsExtension(f private static String encodeTransparenciesAsString(List transparencies) { return transparencies.stream() - .filter(YieldlabBidder::isTransparencyValid) - .map(YieldlabBidder::encodeTransparency) - .collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER)); - } - - private static boolean isTransparencyValid(DsaTransparency transparency) { - return StringUtils.isNotBlank(transparency.getDomain()) - && transparency.getDsaParams() != null - && CollectionUtils.isNotEmpty(transparency.getDsaParams()); + .map(YieldlabBidder::encodeTransparency) + .collect(Collectors.joining(TRANSPARENCY_TEMPLATE_DELIMITER)); } private static String encodeTransparency(DsaTransparency transparency) { - return TRANSPARENCY_TEMPLATE.formatted(transparency.getDomain(), - encodeTransparencyParams(transparency.getDsaParams())); + final String domain = transparency.getDomain(); + if (StringUtils.isBlank(domain)) { + return StringUtils.EMPTY; + } + + final List dsaParams = transparency.getDsaParams(); + if (CollectionUtils.isEmpty(dsaParams)) { + return domain; + } + + return TRANSPARENCY_TEMPLATE.formatted(domain, encodeTransparencyParams(dsaParams)); } private static String encodeTransparencyParams(List dsaParams) { - return dsaParams.stream().map(Objects::toString).collect(Collectors.joining( - TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER)); + return dsaParams.stream() + .map(param -> ObjectUtils.defaultIfNull(param, 0)) + .map(Object::toString) + .collect(Collectors.joining(TRANSPARENCY_TEMPLATE_PARAMS_DELIMITER)); } private static MultiMap resolveHeaders(Site site, Device device, User user) { @@ -336,16 +390,17 @@ private static MultiMap resolveHeaders(Site site, Device device, User user) { .add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON); if (site != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER.toString(), site.getPage()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.REFERER_HEADER, site.getPage()); } if (device != null) { - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER.toString(), device.getUa()); - HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER.toString(), device.getIp()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.USER_AGENT_HEADER, device.getUa()); + HttpUtil.addHeaderIfValueIsNotEmpty(headers, HttpUtil.X_FORWARDED_FOR_HEADER, device.getIp()); } - if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { - headers.add(HttpUtil.COOKIE_HEADER.toString(), "id=" + user.getBuyeruid()); + final String buyerUid = user != null ? user.getBuyeruid() : null; + if (StringUtils.isNotBlank(buyerUid)) { + headers.add(HttpUtil.COOKIE_HEADER, "id=" + buyerUid); } return headers; @@ -353,165 +408,150 @@ private static MultiMap resolveHeaders(Site site, Device device, User user) { @Override public Result> makeBids(BidderCall httpCall, BidRequest bidRequest) { - final List yieldlabResponses; + final List errors = new ArrayList<>(); try { - yieldlabResponses = decodeBodyToBidList(httpCall); - } catch (PreBidException e) { + final List yieldlabBids = mapper.decodeValue( + httpCall.getResponse().getBody(), + YIELDLAB_BID_TYPE_REFERENCE); + return Result.of(extractBids(bidRequest, yieldlabBids, errors), errors); + } catch (DecodeException | PreBidException e) { return Result.withError(BidderError.badServerResponse(e.getMessage())); } + } - final List bidderBids = new ArrayList<>(); - for (int i = 0; i < yieldlabResponses.size(); i++) { - final BidderBid bidderBid; - try { - bidderBid = resolveBidderBid(yieldlabResponses, i, bidRequest); - } catch (PreBidException e) { - return Result.withError(BidderError.badInput(e.getMessage())); - } + private List extractBids(BidRequest bidRequest, + List yieldlabBids, + List errors) { - if (bidderBid != null) { - bidderBids.add(bidderBid); + if (CollectionUtils.isEmpty(yieldlabBids)) { + return Collections.emptyList(); + } + + final Map> adSlotMap = new HashMap<>(); + for (Imp imp : bidRequest.getImp()) { + final ExtImpYieldlab extImpYieldlab = parseImpExt(imp); + if (extImpYieldlab != null) { + adSlotMap.put(extImpYieldlab.getAdslotId(), Pair.of(imp, extImpYieldlab)); } } - return Result.of(bidderBids, Collections.emptyList()); + + return yieldlabBids.stream() + .filter(Objects::nonNull) + .map(bid -> makeBid(bidRequest, bid, adSlotMap, errors)) + .filter(Objects::nonNull) + .toList(); } - private BidderBid resolveBidderBid(List yieldlabResponses, - int currentImpIndex, BidRequest bidRequest) { - final YieldlabResponse yieldlabResponse = yieldlabResponses.get(currentImpIndex); + private BidderBid makeBid(BidRequest bidRequest, + YieldlabBid yieldlabBid, + Map> adSlotMap, + List errors) { - final ExtImpYieldlab matchedExtImp = getMatchedExtImp(yieldlabResponse.getId(), bidRequest.getImp()); - if (matchedExtImp == null) { - throw new PreBidException("Invalid extension"); + final String adSlotId = resolveNumberParameter(yieldlabBid.getId()); + final Pair impPair = adSlotMap.get(adSlotId); + + if (impPair == null) { + throw new PreBidException(("failed to find yieldlab request for adslotID %d. " + + "This is most likely a programming issue").formatted(yieldlabBid.getId())); } - final Imp currentImp = bidRequest.getImp().get(currentImpIndex); - if (currentImp == null) { - throw new PreBidException("Imp not present for id " + currentImpIndex); + final Imp imp = impPair.getKey(); + final ExtImpYieldlab extImp = impPair.getValue(); + final BidType bidType = resolveBidType(imp); + + if (bidType == null) { + return null; } - final Bid.BidBuilder updatedBid = Bid.builder(); - final BidType bidType; - if (currentImp.getVideo() != null) { - bidType = BidType.video; - updatedBid.nurl(makeNurl(bidRequest, matchedExtImp, yieldlabResponse)); - updatedBid.adm(resolveAdm(bidRequest, matchedExtImp, yieldlabResponse)); - } else if (currentImp.getBanner() != null) { - bidType = BidType.banner; - updatedBid.adm(makeAdm(bidRequest, matchedExtImp, yieldlabResponse)); + final Format adsize = resolveAdSize(yieldlabBid.getAdSize()); + final Bid bid = Bid.builder() + .id(adSlotId) + .price(BigDecimal.valueOf(yieldlabBid.getPrice() / 100)) + .impid(imp.getId()) + .crid(makeCreativeId(yieldlabBid, adSlotId)) + .dealid(resolveNumberParameter(yieldlabBid.getPid())) + .nurl(bidType == BidType.video ? makeNurl(bidRequest, extImp, yieldlabBid) : null) + .adm(bidType == BidType.video + ? makeVast(bidRequest, extImp, yieldlabBid) + : makeBanner(bidRequest, extImp, yieldlabBid)) + .w(adsize.getW()) + .h(adsize.getH()) + .ext(resolveBidExt(yieldlabBid, errors)) + .build(); + + return BidderBid.of(bid, bidType, BID_CURRENCY); + } + + private static BidType resolveBidType(Imp imp) { + if (imp.getVideo() != null) { + return BidType.video; + } else if (imp.getBanner() != null) { + return BidType.banner; } else { return null; } - - addBidParams(yieldlabResponse, bidRequest, updatedBid) - .impid(currentImp.getId()); - - return BidderBid.of(updatedBid.build(), bidType, BID_CURRENCY); } - private List decodeBodyToBidList(BidderCall httpCall) { - try { - return mapper.mapper().readValue( - httpCall.getResponse().getBody(), - mapper.mapper().getTypeFactory().constructCollectionType(List.class, YieldlabResponse.class)); - } catch (DecodeException | JsonProcessingException e) { - throw new PreBidException(e.getMessage()); + private static Format resolveAdSize(String adsize) { + if (adsize == null) { + return Format.builder().w(0).h(0).build(); } - } - private ExtImpYieldlab getMatchedExtImp(Integer responseId, List imps) { - return collectImpExt(imps).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()); - - if (matchedExtImp == null) { - throw new PreBidException("Invalid extension"); + final String[] sizes = adsize.split(AD_SIZE_SEPARATOR); + if (sizes.length != 2) { + return Format.builder().w(0).h(0).build(); } - updatedBid.id(resolveNumberParameter(yieldlabResponse.getId())) - .price(resolvePrice(yieldlabResponse.getPrice())) - .dealid(resolveNumberParameter(yieldlabResponse.getPid())) - .crid(makeCreativeId(bidRequest, yieldlabResponse, matchedExtImp)) - .w(resolveSizeParameter(yieldlabResponse.getAdSize(), true)) - .h(resolveSizeParameter(yieldlabResponse.getAdSize(), false)) - .ext(resolveExtParameter(yieldlabResponse)); - - return updatedBid; - } - - private static BigDecimal resolvePrice(Double price) { - return price != null ? BigDecimal.valueOf(price / 100) : null; - } - - private static String resolveNumberParameter(Number param) { - return param != null ? String.valueOf(param) : null; - } - - private static String makeCreativeId(BidRequest bidRequest, YieldlabResponse yieldlabResponse, - ExtImpYieldlab extImp) { - // for passing validation tests - final int weekNumber = isDebugEnabled(bidRequest) ? 35 : Calendar.getInstance().get(Calendar.WEEK_OF_YEAR); - return CREATIVE_ID.formatted(extImp.getAdslotId(), yieldlabResponse.getPid(), weekNumber); - } - - private static Integer resolveSizeParameter(String adSize, boolean isWidth) { - final String[] sizeParts = adSize.split(AD_SIZE_SEPARATOR); - - if (sizeParts.length != 2) { - return 0; + try { + return Format.builder() + .w(Integer.parseUnsignedInt(sizes[0], 10)) + .h(Integer.parseUnsignedInt(sizes[1], 10)) + .build(); + } catch (NumberFormatException e) { + throw new PreBidException("failed to parse yieldlab adsize"); } - final int sizeIndex = isWidth ? 0 : 1; - return StringUtils.isNumeric(sizeParts[sizeIndex]) ? Integer.parseInt(sizeParts[sizeIndex]) : 0; } - private String makeAdm(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { - return AD_SOURCE_BANNER.formatted(makeNurl(bidRequest, extImpYieldlab, yieldlabResponse)); + private static String makeCreativeId(YieldlabBid yieldlabBid, String adSlotId) { + return CREATIVE_ID.formatted(adSlotId, yieldlabBid.getPid(), Calendar.getInstance().get(Calendar.WEEK_OF_YEAR)); } - private String resolveAdm(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { - return VAST_MARKUP.formatted( - extImpYieldlab.getAdslotId(), - makeNurl(bidRequest, extImpYieldlab, yieldlabResponse)); + private String makeBanner(BidRequest bidRequest, ExtImpYieldlab extImp, YieldlabBid yieldlabBid) { + return AD_SOURCE_BANNER.formatted(makeNurl(bidRequest, extImp, yieldlabBid)); } - private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, YieldlabResponse yieldlabResponse) { - // for passing validation tests - final String timestamp = isDebugEnabled(bidRequest) - ? "200000" - : String.valueOf(clock.instant().getEpochSecond()); + private String makeVast(BidRequest bidRequest, ExtImpYieldlab extImp, YieldlabBid yieldlabBid) { + return VAST_MARKUP.formatted(extImp.getAdslotId(), makeNurl(bidRequest, extImp, yieldlabBid)); + } + private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImp, YieldlabBid yieldlabBid) { final URIBuilder uriBuilder = new URIBuilder() - .addParameter("ts", timestamp) - .addParameter("id", extImpYieldlab.getExtId()) - .addParameter("pvid", yieldlabResponse.getPvid()); + .addParameter("ts", resolveNumberParameter(clock.instant().getEpochSecond())) + .addParameter("id", extImp.getExtId()) + .addParameter("pvid", yieldlabBid.getPvid()); final User user = bidRequest.getUser(); if (user != null && StringUtils.isNotBlank(user.getBuyeruid())) { - uriBuilder.addParameter("ids", String.join("ylid:", user.getBuyeruid())); + uriBuilder.addParameter("ids", "ylid:" + StringUtils.defaultString(user.getBuyeruid())); } final String gdpr = getGdprParameter(bidRequest.getRegs()); final String consent = getConsentParameter(bidRequest.getUser()); if (StringUtils.isNotBlank(gdpr) && StringUtils.isNotBlank(consent)) { - uriBuilder.addParameter("gdpr", gdpr) - .addParameter("consent", consent); + uriBuilder + .addParameter("gdpr", gdpr) + .addParameter("gdpr_consent", consent); } return AD_SOURCE_URL.formatted( - extImpYieldlab.getAdslotId(), - extImpYieldlab.getSupplyId(), - yieldlabResponse.getAdSize(), + extImp.getAdslotId(), + extImp.getSupplyId(), + yieldlabBid.getAdSize(), uriBuilder.toString().replace("?", "")); } - private ObjectNode resolveExtParameter(YieldlabResponse yieldlabResponse) { - final YieldlabDigitalServicesActResponse dsa = yieldlabResponse.getDsa(); + private ObjectNode resolveBidExt(YieldlabBid bid, List errors) { + final ExtBidDsa dsa = bid.getDsa(); if (dsa == null) { return null; } @@ -520,10 +560,15 @@ private ObjectNode resolveExtParameter(YieldlabResponse yieldlabResponse) { try { dsaNode = mapper.mapper().valueToTree(dsa); } catch (IllegalArgumentException e) { - logger.error("Failed to serialize DSA object for adslot {}", yieldlabResponse.getId(), e); + errors.add(BidderError.badServerResponse( + "Failed to serialize DSA object for adslot %d".formatted(bid.getId()))); return null; } ext.set("dsa", dsaNode); return ext; } + + private static String resolveNumberParameter(Number param) { + return param != null ? String.valueOf(param) : null; + } } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabBid.java similarity index 68% rename from src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java rename to src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabBid.java index 9a2b54652ad..335a4fd9672 100644 --- a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabResponse.java +++ b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabBid.java @@ -3,12 +3,13 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.AllArgsConstructor; import lombok.Value; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; @AllArgsConstructor(staticName = "of") @Value -public class YieldlabResponse { +public class YieldlabBid { - Integer id; + Long id; Double price; @@ -17,11 +18,11 @@ public class YieldlabResponse { @JsonProperty("adsize") String adSize; - Integer pid; + Long pid; - Integer did; + Long did; String pvid; - YieldlabDigitalServicesActResponse dsa; + ExtBidDsa dsa; } diff --git a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java b/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java deleted file mode 100644 index 4fd18d813df..00000000000 --- a/src/main/java/org/prebid/server/bidder/yieldlab/model/YieldlabDigitalServicesActResponse.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.prebid.server.bidder.yieldlab.model; - -import lombok.AllArgsConstructor; -import lombok.Value; - -import java.util.List; - -@AllArgsConstructor(staticName = "of") -@Value(staticConstructor = "of") -public class YieldlabDigitalServicesActResponse { - - String behalf; - - String paid; - - Integer adrender; - - List transparency; - - @AllArgsConstructor(staticName = "of") - @Value(staticConstructor = "of") - public static class Transparency { - String domain; - List dsaparams; - } -} 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 a445a285955..c230df8f9ca 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; @@ -23,15 +26,16 @@ import org.prebid.server.bidder.model.HttpRequest; import org.prebid.server.bidder.model.HttpResponse; import org.prebid.server.bidder.model.Result; -import org.prebid.server.bidder.yieldlab.model.YieldlabDigitalServicesActResponse; -import org.prebid.server.bidder.yieldlab.model.YieldlabResponse; +import org.prebid.server.bidder.yieldlab.model.YieldlabBid; 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.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; +import org.prebid.server.proto.openrtb.ext.response.ExtBidDsa; import java.math.BigDecimal; import java.time.Clock; @@ -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&consent=consent"); + assertThat(uri).endsWith("&t=key1%3Dvalue1%26key2%3Dvalue2&sizes=1%3A1x1%7C2x2&" + + "ids=ylid%3Abuyeruid&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); }); @@ -156,6 +175,7 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { final List imps = new ArrayList<>(); imps.add(Imp.builder() + .id("impId1") .banner(Banner.builder().w(1).h(1).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() @@ -166,6 +186,7 @@ public void constructExtImpShouldWorkWithDuplicateKeysTargeting() { .build()))) .build()); imps.add(Imp.builder() + .id("impId2") .banner(Banner.builder().w(1).h(1).build()) .ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder() @@ -192,8 +213,8 @@ 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&" - + "yl_rtb_devicetype=1&gdpr=1&consent=consent"); + assertThat(uri).endsWith("&t=key1%3Dvalue1&sizes=1%3A%2C2%3A&ids=ylid%3Abuyeruid&yl_rtb_ifa&" + + "yl_rtb_devicetype=1&gdpr=1&gdpr_consent=consent"); }); } @@ -208,7 +229,7 @@ public void makeBidsShouldReturnErrorIfResponseBodyCouldNotBeParsed() { // then assertThat(result.getErrors()).hasSize(1); assertThat(result.getErrors()).allMatch(error -> error.getType() == BidderError.Type.bad_server_response - && error.getMessage().startsWith("Unrecognized token 'invalid")); + && error.getMessage().startsWith("Failed to decode: Unrecognized token 'invalid'")); assertThat(result.getValue()).isEmpty(); } @@ -228,12 +249,12 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio .build())) .device(Device.builder().ip("ip").ua("Agent").language("fr").devicetype(1).build()) .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()) + .user(User.builder().ext(ExtUser.builder().consent("consent").build()).build()) .site(Site.builder().page("http://www.example.com").build()) .build(); - final YieldlabResponse yieldlabResponse = YieldlabResponse.of(1, 201d, "yieldlab", - "728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", null); + final YieldlabBid yieldlabResponse = YieldlabBid.of(1L, 201d, "yieldlab", + "728x90", 1234L, 5678L, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", null); final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse)); @@ -245,7 +266,7 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio final int weekNumber = Calendar.getInstance().get(Calendar.WEEK_OF_YEAR); final String adm = """ """.formatted(timestamp); final BidderBid expected = BidderBid.of( Bid.builder() @@ -279,8 +300,8 @@ public void makeBidsShouldReturnCorrectAdm() throws JsonProcessingException { .build())) .build(); - final YieldlabResponse yieldlabResponse = YieldlabResponse.of(12345, 201d, "yieldlab", - "728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", null); + final YieldlabBid yieldlabResponse = YieldlabBid.of(12345L, 201d, "yieldlab", + "728x90", 1234L, 5678L, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", null); final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse)); @@ -458,21 +479,15 @@ public void makeBidsShouldAddDsaParamsWhenDsaIsPresentInResponse() throws JsonPr .build())) .build(); - final YieldlabDigitalServicesActResponse dsaResponse = YieldlabDigitalServicesActResponse.of( - "yieldlab", - "yieldlab", - 2, - List.of( - YieldlabDigitalServicesActResponse.Transparency.of( - "yieldlab.de", - List.of(1, 2, 3) - ) - ) - ); + final ExtBidDsa dsaResponse = ExtBidDsa.builder() + .paid("yieldlab") + .behalf("yieldlab") + .adRender(2) + .transparency(List.of(DsaTransparency.of("yieldlab.de", List.of(1, 2, 3)))) + .build(); - final YieldlabResponse yieldlabResponse = YieldlabResponse.of(1, 201d, "yieldlab", - "728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", dsaResponse - ); + final YieldlabBid yieldlabResponse = YieldlabBid.of(1L, 201d, "yieldlab", + "728x90", 1234L, 5678L, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", dsaResponse); final BidderCall httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse));