Skip to content

Commit

Permalink
Yieldlab: Implementing DSA support
Browse files Browse the repository at this point in the history
  • Loading branch information
William Harris committed Feb 23, 2024
1 parent 7672f9e commit cc0cca6
Show file tree
Hide file tree
Showing 4 changed files with 265 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

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;
Expand All @@ -23,11 +25,13 @@
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.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.ExtRegsDsaTransparency;
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.ExtUser;
Expand All @@ -41,6 +45,7 @@
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
Expand Down Expand Up @@ -189,6 +194,8 @@ private String makeUrl(ExtImpYieldlab extImpYieldlab, BidRequest request) {
uriBuilder.addParameter("consent", consent);
}

extractDsaRequestParams(request).forEach(uriBuilder::addParameter);

return uriBuilder.toString();
}

Expand Down Expand Up @@ -231,6 +238,55 @@ private static String getConsentParameter(User user) {
return ObjectUtils.defaultIfNull(consent, "");
}

private static Map<String, String> extractDsaRequestParams(BidRequest request) {
if (!hasDsa(request)) {
return Map.of();
}

final var dsa = request.getRegs().getExt().getDsa();
final var dsaRequestParams = new HashMap<String, String>();

if (dsa.getDsaRequired() != null) {
dsaRequestParams.put("dsarequired", dsa.getDsaRequired().toString());
}

if (dsa.getPubRender() != null) {
dsaRequestParams.put("dsapubrender", dsa.getPubRender().toString());
}

if (dsa.getDataToPub() != null) {
dsaRequestParams.put("dsadatatopub", dsa.getDataToPub().toString());
}

if (dsa.getTransparency() != null && !dsa.getTransparency().isEmpty()) {
dsaRequestParams.put("dsatransparency", encodeTransparenciesAsString(dsa.getTransparency()));
}

return dsaRequestParams.entrySet().stream()
.filter(entry -> !entry.getValue().isBlank())
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

private static boolean hasDsa(BidRequest request) {
return request.getRegs() != null
&& request.getRegs().getExt() != null
&& request.getRegs().getExt().getDsa() != null;
}

private static String encodeTransparenciesAsString(List<ExtRegsDsaTransparency> transparency) {
return transparency.stream()
.map(YieldlabBidder::encodeTransparency)
.collect(Collectors.joining("~~"));
}

private static String encodeTransparency(ExtRegsDsaTransparency transparency) {
return "%s~%s".formatted(transparency.getDomain(), encodeTransparencyParams(transparency.getDsaParams()));
}

private static String encodeTransparencyParams(List<Integer> dsaParams) {
return dsaParams.stream().map(Objects::toString).collect(Collectors.joining("_"));
}

private static MultiMap resolveHeaders(Site site, Device device, User user) {
final MultiMap headers = MultiMap.caseInsensitiveMultiMap()
.add(HttpUtil.ACCEPT_HEADER, HttpHeaderValues.APPLICATION_JSON);
Expand Down Expand Up @@ -339,7 +395,8 @@ private Bid.BidBuilder addBidParams(YieldlabResponse yieldlabResponse, BidReques
.dealid(resolveNumberParameter(yieldlabResponse.getPid()))
.crid(makeCreativeId(bidRequest, yieldlabResponse, matchedExtImp))
.w(resolveSizeParameter(yieldlabResponse.getAdSize(), true))
.h(resolveSizeParameter(yieldlabResponse.getAdSize(), false));
.h(resolveSizeParameter(yieldlabResponse.getAdSize(), false))
.ext(resolveExtParameter(yieldlabResponse.getDsa()));

return updatedBid;
}
Expand Down Expand Up @@ -408,4 +465,14 @@ private String makeNurl(BidRequest bidRequest, ExtImpYieldlab extImpYieldlab, Yi
yieldlabResponse.getAdSize(),
uriBuilder.toString().replace("?", ""));
}

private ObjectNode resolveExtParameter(YieldlabDigitalServicesActResponse dsa) {
if (dsa == null) {
return null;
}
final var ext = mapper.mapper().createObjectNode();
final var dsaNode = mapper.mapper().convertValue(dsa, JsonNode.class);
ext.set("dsa", dsaNode);
return ext;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.prebid.server.bidder.yieldlab.model;

import lombok.AllArgsConstructor;
import lombok.Value;

import java.util.List;

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

String behalf;
String paid;
Integer adrender;
List<Transparency> transparency;

@AllArgsConstructor(staticName = "of")
@Value
public static class Transparency {
String domain;
List<Integer> dsaparams;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ public class YieldlabResponse {
Integer did;

String pvid;

YieldlabDigitalServicesActResponse dsa;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.prebid.server.bidder.yieldlab;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.iab.openrtb.request.Banner;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
Expand All @@ -12,20 +13,27 @@
import com.iab.openrtb.response.Bid;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.prebid.server.VertxTest;
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.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.proto.openrtb.ext.ExtPrebid;
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.ExtRegsDsaTransparency;
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 java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.time.Clock;
import java.time.Instant;
Expand All @@ -35,12 +43,14 @@
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static java.util.Collections.singletonList;
import static java.util.Collections.singletonMap;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.assertj.core.api.Assertions.tuple;
import static org.junit.jupiter.params.provider.Arguments.arguments;

public class YieldlabBidderTest extends VertxTest {

Expand Down Expand Up @@ -226,7 +236,7 @@ public void makeBidsShouldReturnCorrectBidderBid() throws JsonProcessingExceptio
.build();

final YieldlabResponse yieldlabResponse = YieldlabResponse.of(1, 201d, "yieldlab",
"728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5");
"728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", null);

final BidderCall<Void> httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse));

Expand Down Expand Up @@ -274,7 +284,7 @@ public void makeBidsShouldReturnCorrectAdm() throws JsonProcessingException {
.build();

final YieldlabResponse yieldlabResponse = YieldlabResponse.of(12345, 201d, "yieldlab",
"728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5");
"728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", null);

final BidderCall<Void> httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse));

Expand All @@ -301,6 +311,166 @@ public void makeBidsShouldReturnCorrectAdm() throws JsonProcessingException {
.containsExactly(expectedAdm);
}

public static Stream<? extends Arguments> dsaRequestCases() {
return Stream.of(
arguments(
"DSA is forwarded",
ExtRegsDsa.of(
1, 2, 3, List.of(ExtRegsDsaTransparency.of("testDomain", List.of(1, 2, 3)))
),
Map.of(
"dsarequired", "1",
"dsapubrender", "2",
"dsadatatopub", "3",
"dsatransparency", "testDomain~1_2_3"
)
),
arguments(
"DSA with multiple transparencies is encoded correctly",
ExtRegsDsa.of(
1, 2, 3, List.of(
ExtRegsDsaTransparency.of("testDomain", List.of(1, 2, 3)),
ExtRegsDsaTransparency.of("testDomain2", List.of(4, 5, 6))
)
),
Map.of(
"dsarequired", "1",
"dsapubrender", "2",
"dsadatatopub", "3",
"dsatransparency", "testDomain~1_2_3~~testDomain2~4_5_6"
)

),
arguments(
"Missing values are not forwarded",
ExtRegsDsa.of(
2, null, 3, null
),
Map.of(
"dsarequired", "2",
"dsadatatopub", "3"
)
)
);
}

@ParameterizedTest(name = "{index} - {0}")
@MethodSource("dsaRequestCases")
public void dsaIsForwarded(String testName, ExtRegsDsa dsa, Map<String, String> expectations)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//given
final var regs = Regs.builder()
.ext(ExtRegs.of(null, null, null, dsa))
.build();

final var bidRequest = BidRequest.builder()
.regs(regs)
.build();

final var getDsaRequestParams =
YieldlabBidder.class.getDeclaredMethod("extractDsaRequestParams", BidRequest.class);
getDsaRequestParams.setAccessible(true);

//when
final var dsaRequestParams = (HashMap<String, String>) getDsaRequestParams.invoke(null, bidRequest);

//then
assertThat(dsaRequestParams).containsExactlyInAnyOrderEntriesOf(expectations);
}

@Test
public void dsaParamsAreSentInRequest() {
//given
final var transparencies = List.of(
ExtRegsDsaTransparency.of("testDomain", List.of(1, 2, 3)),
ExtRegsDsaTransparency.of("testDomain2", List.of(4, 5, 6))
);
final var dsa = ExtRegsDsa.of(
1, 2, 3, transparencies);
final var regs = Regs.builder()
.ext(ExtRegs.of(null, null, null, dsa))
.build();
final BidRequest bidRequest = BidRequest.builder()
.imp(singletonList(Imp.builder()
.ext(mapper.valueToTree(ExtPrebid.of(null,
ExtImpYieldlab.builder()
.adslotId("123")
.build())))
.build()))
.regs(regs)
.build();

//when
final var result = target.makeHttpRequests(bidRequest);

//then
assertThat(result.getValue().get(0).getUri())
.contains(
"&dsarequired=1",
"&dsapubrender=2",
"&dsadatatopub=3",
"&dsatransparency=testDomain%7E1_2_3%7E%7EtestDomain2%7E4_5_6"
);
}

@Test
public void dsaParamsIsAddedToResponse() throws JsonProcessingException {
// given
final BidRequest bidRequest = BidRequest.builder()
.imp(singletonList(Imp.builder()
.id("test-imp-id")
.banner(Banner.builder().w(1).h(1).build())
.ext(mapper.valueToTree(ExtPrebid.of(null, ExtImpYieldlab.builder()
.adslotId("1")
.supplyId("2")
.adSize("adSize")
.targeting(singletonMap("key", "value"))
.extId("extId")
.build())))
.build()))
.build();

final var dsaResponse = YieldlabDigitalServicesActResponse.of(
"yieldlab",
"yieldlab",
2,
List.of(
YieldlabDigitalServicesActResponse.Transparency.of(
"yieldlab.de",
List.of(1, 2, 3)
)
)
);

final var yieldlabResponse = YieldlabResponse.of(1, 201d, "yieldlab",
"728x90", 1234, 5678, "40cb3251-1e1e-4cfd-8edc-7d32dc1a21e5", dsaResponse
);

final var httpCall = givenHttpCall(mapper.writeValueAsString(yieldlabResponse));

// when
final var result = target.makeBids(httpCall, bidRequest);

// then
final var expectedTransparency = mapper.createObjectNode();
expectedTransparency.put("domain", "yieldlab.de");
expectedTransparency.set("dsaparams", mapper.convertValue(List.of(1, 2, 3), JsonNode.class));

final var transparencies = mapper.createArrayNode();
transparencies.add(expectedTransparency);

final var expectedDsa = mapper.createObjectNode();
expectedDsa.put("paid", "yieldlab");
expectedDsa.put("behalf", "yieldlab");
expectedDsa.put("adrender", 2);
expectedDsa.set("transparency", transparencies);

final var actualDsa = result.getValue().get(0).getBid().getExt().get("dsa");

assertThat(result.getErrors()).isEmpty();
assertThat(actualDsa).isEqualTo(expectedDsa);
}

private static BidderCall<Void> givenHttpCall(String body) {
return BidderCall.succeededHttp(
HttpRequest.<Void>builder().build(),
Expand Down

0 comments on commit cc0cca6

Please sign in to comment.