Skip to content

Pubmatic: Support Alternate Bidder Codes #4113

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

Merged
merged 1 commit into from
Aug 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 57 additions & 11 deletions src/main/java/org/prebid/server/bidder/pubmatic/PubmaticBidder.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.SeatBid;
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.apache.commons.lang3.tuple.Pair;
import org.prebid.server.auction.aliases.AlternateBidder;
import org.prebid.server.auction.aliases.AlternateBidderCodesConfig;
import org.prebid.server.bidder.Bidder;
import org.prebid.server.bidder.model.BidderBid;
import org.prebid.server.bidder.model.BidderCall;
Expand All @@ -25,6 +28,7 @@
import org.prebid.server.bidder.model.Result;
import org.prebid.server.bidder.pubmatic.model.request.PubmaticBidderImpExt;
import org.prebid.server.bidder.pubmatic.model.request.PubmaticExtDataAdServer;
import org.prebid.server.bidder.pubmatic.model.request.PubmaticMarketplace;
import org.prebid.server.bidder.pubmatic.model.request.PubmaticWrapper;
import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidExt;
import org.prebid.server.bidder.pubmatic.model.response.PubmaticBidResponse;
Expand Down Expand Up @@ -59,6 +63,7 @@
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand All @@ -70,6 +75,7 @@ public class PubmaticBidder implements Bidder<BidRequest> {
private static final String IMP_EXT_AD_UNIT_KEY = "dfp_ad_unit_code";
private static final String AD_SERVER_GAM = "gam";
private static final String PREBID = "prebid";
private static final String MARKETPLACE_EXT_REQUEST = "marketplace";
private static final String ACAT_EXT_REQUEST = "acat";
private static final String WRAPPER_EXT_REQUEST = "wrapper";
private static final String BIDDER_NAME = "pubmatic";
Expand All @@ -79,6 +85,8 @@ public class PubmaticBidder implements Bidder<BidRequest> {
private static final String IMP_EXT_ADSERVER = "adserver";
private static final List<String> IMP_EXT_DATA_RESERVED_FIELD = List.of(IMP_EXT_PBADSLOT, IMP_EXT_ADSERVER);
private static final String DCTR_VALUE_FORMAT = "%s=%s";
private static final String WILDCARD = "*";
private static final String WILDCARD_ALL = "all";

private final String endpointUrl;
private final JacksonMapper mapper;
Expand All @@ -97,10 +105,13 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
PubmaticWrapper wrapper;
final List<String> acat;
final Pair<String, String> displayManagerFields;
final List<String> allowedBidders;

try {
acat = extractAcat(request);
wrapper = extractWrapper(request);
final JsonNode bidderparams = getExtRequestPrebidBidderparams(request);
acat = extractAcat(bidderparams);
wrapper = extractWrapper(bidderparams);
allowedBidders = extractAllowedBidders(request);
displayManagerFields = extractDisplayManagerFields(request.getApp());
} catch (IllegalArgumentException e) {
return Result.withError(BidderError.badInput(e.getMessage()));
Expand Down Expand Up @@ -129,12 +140,43 @@ public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request
return Result.withErrors(errors);
}

final BidRequest modifiedBidRequest = modifyBidRequest(request, validImps, publisherId, wrapper, acat);
final BidRequest modifiedBidRequest = modifyBidRequest(
request, validImps, publisherId, wrapper, acat, allowedBidders);
return Result.of(Collections.singletonList(makeHttpRequest(modifiedBidRequest)), errors);
}

private List<String> extractAcat(BidRequest request) {
final JsonNode bidderParams = getExtRequestPrebidBidderparams(request);
private List<String> extractAllowedBidders(BidRequest request) {
final AlternateBidderCodesConfig alternateBidderCodes = Optional.ofNullable(request.getExt())
.map(ExtRequest::getPrebid)
.map(ExtRequestPrebid::getAlternateBidderCodes)
.orElse(null);

if (alternateBidderCodes == null) {
return null;
}

if (BooleanUtils.isNotTrue(alternateBidderCodes.getEnabled())) {
return Collections.singletonList(BIDDER_NAME);
}

final AlternateBidder alternateBidder = Optional.ofNullable(alternateBidderCodes.getBidders())
.map(bidders -> bidders.get(BIDDER_NAME))
.filter(bidder -> BooleanUtils.isTrue(bidder.getEnabled()))
.orElse(null);

if (alternateBidder == null) {
return Collections.singletonList(BIDDER_NAME);
}

final Set<String> allowedBidderCodes = alternateBidder.getAllowedBidderCodes();
if (allowedBidderCodes == null || allowedBidderCodes.contains(WILDCARD)) {
return Collections.singletonList(WILDCARD_ALL);
}

return Stream.concat(Stream.of(BIDDER_NAME), allowedBidderCodes.stream()).toList();
}

private List<String> extractAcat(JsonNode bidderParams) {
final JsonNode acatNode = bidderParams != null ? bidderParams.get(ACAT_EXT_REQUEST) : null;

return acatNode != null && acatNode.isArray()
Expand All @@ -144,9 +186,8 @@ private List<String> extractAcat(BidRequest request) {
: null;
}

private PubmaticWrapper extractWrapper(BidRequest request) {
final JsonNode pubmatic = getExtRequestPrebidBidderparams(request);
final JsonNode wrapperNode = pubmatic != null ? pubmatic.get(WRAPPER_EXT_REQUEST) : null;
private PubmaticWrapper extractWrapper(JsonNode bidderParams) {
final JsonNode wrapperNode = bidderParams != null ? bidderParams.get(WRAPPER_EXT_REQUEST) : null;

return wrapperNode != null && wrapperNode.isObject()
? mapper.mapper().convertValue(wrapperNode, PubmaticWrapper.class)
Expand Down Expand Up @@ -433,13 +474,14 @@ private BidRequest modifyBidRequest(BidRequest request,
List<Imp> imps,
String publisherId,
PubmaticWrapper wrapper,
List<String> acat) {
List<String> acat,
List<String> allowedBidders) {

return request.toBuilder()
.imp(imps)
.site(modifySite(request.getSite(), publisherId))
.app(modifyApp(request.getApp(), publisherId))
.ext(modifyExtRequest(wrapper, acat))
.ext(modifyExtRequest(wrapper, acat, allowedBidders))
.build();
}

Expand All @@ -465,7 +507,7 @@ private static Publisher modifyPublisher(Publisher publisher, String publisherId
: Publisher.builder().id(publisherId).build();
}

private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List<String> acat) {
private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List<String> acat, List<String> allowedBidders) {
final ObjectNode extNode = mapper.mapper().createObjectNode();

if (wrapper != null) {
Expand All @@ -476,6 +518,10 @@ private ExtRequest modifyExtRequest(PubmaticWrapper wrapper, List<String> acat)
extNode.putPOJO(ACAT_EXT_REQUEST, acat);
}

if (allowedBidders != null) {
extNode.putPOJO(MARKETPLACE_EXT_REQUEST, PubmaticMarketplace.of(allowedBidders));
}

final ExtRequest newExtRequest = ExtRequest.empty();
return extNode.isEmpty()
? newExtRequest
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.prebid.server.bidder.pubmatic.model.request;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Value;

import java.util.List;

@Value(staticConstructor = "of")
public class PubmaticMarketplace {

@JsonProperty("allowedbidders")
List<String> allowedBidders;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.prebid.server.bidder.pubmatic;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.node.TextNode;
import com.iab.openrtb.request.App;
Expand All @@ -17,6 +19,7 @@
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import io.vertx.core.http.HttpMethod;
import org.assertj.core.api.InstanceOfAssertFactories;
import org.junit.jupiter.api.Test;
import org.prebid.server.VertxTest;
import org.prebid.server.bidder.model.BidderBid;
Expand All @@ -38,6 +41,8 @@
import org.prebid.server.proto.openrtb.ext.request.ExtAppPrebid;
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.ExtRequestPrebidAlternateBidderCodes;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidAlternateBidderCodesBidder;
import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmatic;
import org.prebid.server.proto.openrtb.ext.request.pubmatic.ExtImpPubmaticKeyVal;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
Expand All @@ -50,6 +55,7 @@
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.UnaryOperator;

import static java.util.Arrays.asList;
Expand Down Expand Up @@ -214,6 +220,175 @@ public void makeHttpRequestsShouldReturnBidRequestExtIfAcatFieldIsValidAndTrimWh
.containsExactly(expectedExtRequest);
}

@Test
public void makeHttpRequestsShouldReturnAllowedBidderCodeWithPubmaticAdded() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic",
ExtRequestPrebidAlternateBidderCodesBidder.of(true, Set.of("bidder1", "bidder2")))))
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders"))
.asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class))
.extracting(JsonNode::asText)
.containsOnly("pubmatic", "bidder1", "bidder2");
}

@Test
public void makeHttpRequestsShouldReturnOnlyPubmaticWhenPubmaticCodesAreDisabled() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic",
ExtRequestPrebidAlternateBidderCodesBidder.of(false, Set.of("bidder1", "bidder2")))))
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders"))
.asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class))
.extracting(JsonNode::asText)
.containsOnly("pubmatic");
}

@Test
public void makeHttpRequestsShouldReturnOnlyAllWhenPubmaticCodesAreAbsent() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic",
ExtRequestPrebidAlternateBidderCodesBidder.of(true, null))))
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders"))
.asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class))
.extracting(JsonNode::asText)
.containsOnly("all");
}

@Test
public void makeHttpRequestsShouldReturnOnlyAllWhenPubmaticCodesHasWildcard() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of("pubmatic",
ExtRequestPrebidAlternateBidderCodesBidder.of(true, Set.of("*")))))
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders"))
.asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class))
.extracting(JsonNode::asText)
.containsOnly("all");
}

@Test
public void makeHttpRequestsShouldReturnOnlyPubmaticWhenPubmaticCodesAreAbsent() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(true, Map.of()))
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders"))
.asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class))
.extracting(JsonNode::asText)
.containsOnly("pubmatic");
}

@Test
public void makeHttpRequestsShouldReturnOnlyPubmaticWhenAlternateBidderCodesAreDisabled() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(ExtRequestPrebidAlternateBidderCodes.of(false, Map.of()))
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.extracting(marketplace -> (ArrayNode) marketplace.path("allowedbidders"))
.asInstanceOf(InstanceOfAssertFactories.iterable(JsonNode.class))
.extracting(JsonNode::asText)
.containsOnly("pubmatic");
}

@Test
public void makeHttpRequestsShouldReturnNothingWhenAlternateBidderCodeIsAbsent() {
// given
final ExtRequest bidRequestExt = ExtRequest.of(ExtRequestPrebid.builder()
.alternateBidderCodes(null)
.build());
final BidRequest bidRequest = givenBidRequest(
bidRequestBuilder -> bidRequestBuilder.ext(bidRequestExt), identity(), identity());

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

// then
assertThat(result.getErrors()).isEmpty();
assertThat(result.getValue()).hasSize(1).first()
.extracting(HttpRequest::getPayload)
.extracting(BidRequest::getExt)
.extracting(ext -> ext.getProperty("marketplace"))
.isNull();
}

@Test
public void makeHttpRequestsShouldMergeWrappersFromImpAndBidRequestExt() {
// given
Expand Down
Loading