Skip to content
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
2 changes: 1 addition & 1 deletion extra/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
<checkstyle.version>10.17.0</checkstyle.version>

<!-- Project production dependency versions -->
<spring.boot.version>3.4.4</spring.boot.version>
<spring.boot.version>3.4.2</spring.boot.version>
<vertx.version>4.5.14</vertx.version>
<validation-api.version>2.0.1.Final</validation-api.version>
<commons.collections.version>4.4</commons.collections.version>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,10 +261,12 @@ private Bid updateBidWithId(Bid bid) {
private static BidType getType(String impId, List<Imp> imps) {
for (Imp imp : imps) {
if (imp.getId().equals(impId)) {
if (imp.getVideo() != null) {
return BidType.video;
} else if (imp.getAudio() != null) {
if (imp.getAudio() != null) {
return BidType.audio;
} else if (imp.getXNative() != null) {
return BidType.xNative;
} else if (imp.getVideo() != null) {
return BidType.video;
} else {
return BidType.banner;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package org.prebid.server.bidder.zeta_global_ssp;

import com.fasterxml.jackson.core.type.TypeReference;
import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Imp;
import com.iab.openrtb.response.Bid;
import com.iab.openrtb.response.BidResponse;
import com.iab.openrtb.response.SeatBid;
import org.prebid.server.bidder.Bidder;
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.Result;
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.zeta_global_ssp.ExtImpZetaGlobalSSP;
import org.prebid.server.proto.openrtb.ext.response.BidType;
import org.prebid.server.proto.openrtb.ext.response.ExtBidPrebid;
import org.prebid.server.util.BidderUtil;
import org.prebid.server.util.HttpUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

public class ZetaGlobalSspBidder implements Bidder<BidRequest> {

private static final TypeReference<ExtPrebid<?, ExtImpZetaGlobalSSP>> ZETA_GLOBAL_EXT_TYPE_REFERENCE =
new TypeReference<>() {
};

private static final TypeReference<ExtPrebid<ExtBidPrebid, ?>> EXT_BID_TYPE_REFERENCE =
new TypeReference<>() {
};
private static final String SID_MACRO = "{{AccountID}}";

private final String endpointUrl;
private final JacksonMapper mapper;

public ZetaGlobalSspBidder(String endpointUrl, JacksonMapper mapper) {
this.endpointUrl = HttpUtil.validateUrl(endpointUrl);
this.mapper = Objects.requireNonNull(mapper);
}

@Override
public Result<List<HttpRequest<BidRequest>>> makeHttpRequests(BidRequest request) {
final Imp firstImp = request.getImp().getFirst();
final ExtImpZetaGlobalSSP extImp;

try {
extImp = parseImpExt(firstImp);
} catch (PreBidException e) {
return Result.withError(BidderError.badInput(e.getMessage()));
}

final HttpRequest<BidRequest> httpRequest = BidderUtil.defaultRequest(
removeImpsExt(request),
resolveEndpoint(extImp),
mapper);

return Result.withValues(Collections.singletonList(httpRequest));
}

private ExtImpZetaGlobalSSP parseImpExt(Imp imp) {
try {
return mapper.mapper().convertValue(imp.getExt(), ZETA_GLOBAL_EXT_TYPE_REFERENCE).getBidder();
} catch (IllegalArgumentException e) {
throw new PreBidException("Missing bidder ext in impression with id: " + imp.getId());
}
}

private String resolveEndpoint(ExtImpZetaGlobalSSP extImpZetaGlobalSSP) {
return endpointUrl
.replace(SID_MACRO, Objects.toString(extImpZetaGlobalSSP.getSid(), "0"));
}

private BidRequest removeImpsExt(BidRequest request) {
final List<Imp> imps = new ArrayList<>(request.getImp());
final Imp firstImp = imps.getFirst().toBuilder().ext(null).build();
imps.set(0, firstImp);

return request.toBuilder()
.imp(imps)
.build();
}

@Override
public Result<List<BidderBid>> makeBids(BidderCall<BidRequest> httpCall, BidRequest bidRequest) {
try {
final BidResponse bidResponse = mapper.decodeValue(httpCall.getResponse().getBody(), BidResponse.class);
final List<BidderError> errors = new ArrayList<>();
return Result.of(extractBids(bidResponse, errors), errors);
} catch (DecodeException | PreBidException e) {
return Result.withError(BidderError.badServerResponse(e.getMessage()));
}
}

private List<BidderBid> extractBids(BidResponse bidResponse, List<BidderError> errors) {
if (bidResponse == null || bidResponse.getSeatbid() == null) {
return Collections.emptyList();
}
return bidsFromResponse(bidResponse, errors);
}

private List<BidderBid> bidsFromResponse(BidResponse bidResponse, List<BidderError> errors) {
return bidResponse.getSeatbid().stream()
.filter(Objects::nonNull)
.map(SeatBid::getBid)
.filter(Objects::nonNull)
.flatMap(Collection::stream)
.filter(Objects::nonNull)
.map(bid -> makeBid(bid, bidResponse.getCur(), errors))
.filter(Objects::nonNull)
.toList();
}

private BidderBid makeBid(Bid bid, String currency, List<BidderError> errors) {
final BidType mediaType = getMediaType(bid, errors);
return mediaType == null ? null : BidderBid.of(bid, mediaType, currency);
}

private BidType getMediaType(Bid bid, List<BidderError> errors) {
try {
return Optional.ofNullable(bid.getExt())
.map(ext -> mapper.mapper().convertValue(ext, EXT_BID_TYPE_REFERENCE))
.map(ExtPrebid::getPrebid)
.map(ExtBidPrebid::getType)
.orElseThrow(IllegalArgumentException::new);
} catch (IllegalArgumentException e) {
errors.add(BidderError.badServerResponse(
"Failed to parse impression \"%s\" mediatype".formatted(bid.getImpid())));
return null;
}
}
}
126 changes: 82 additions & 44 deletions src/main/java/org/prebid/server/floors/BasicPriceFloorProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.Logger;
import org.prebid.server.log.LoggerFactory;
import org.prebid.server.metric.MetricName;
import org.prebid.server.metric.Metrics;
import org.prebid.server.proto.openrtb.ext.request.ExtImpPrebidFloors;
import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
Expand Down Expand Up @@ -50,19 +52,35 @@ public class BasicPriceFloorProcessor implements PriceFloorProcessor {
private static final int MODEL_WEIGHT_MAX_VALUE = 100;
private static final int MODEL_WEIGHT_MIN_VALUE = 1;

private static final String FETCH_FAILED_ERROR_MESSAGE = "Price floors processing failed: %s. "
+ "Following parsing of request price floors is failed: %s";
private static final String DYNAMIC_DATA_NOT_ALLOWED_MESSAGE =
"Price floors processing failed: Using dynamic data is not allowed. "
+ "Following parsing of request price floors is failed: %s";
private static final String INVALID_REQUEST_WARNING_MESSAGE =
"Price floors processing failed: parsing of request price floors is failed: %s";
private static final String ERROR_LOG_MESSAGE =
"Price Floors can't be resolved for account %s and request %s, reason: %s";

private final PriceFloorFetcher floorFetcher;
private final PriceFloorResolver floorResolver;
private final Metrics metrics;
private final JacksonMapper mapper;
private final double logSamplingRate;

private final RandomWeightedEntrySupplier<PriceFloorModelGroup> modelPicker;

public BasicPriceFloorProcessor(PriceFloorFetcher floorFetcher,
PriceFloorResolver floorResolver,
JacksonMapper mapper) {
Metrics metrics,
JacksonMapper mapper,
double logSamplingRate) {

this.floorFetcher = Objects.requireNonNull(floorFetcher);
this.floorResolver = Objects.requireNonNull(floorResolver);
this.metrics = Objects.requireNonNull(metrics);
this.mapper = Objects.requireNonNull(mapper);
this.logSamplingRate = logSamplingRate;

modelPicker = new RandomPositiveWeightedEntrySupplier<>(BasicPriceFloorProcessor::resolveModelGroupWeight);
}
Expand All @@ -82,7 +100,7 @@ public BidRequest enrichWithPriceFloors(BidRequest bidRequest,
return disableFloorsForRequest(bidRequest);
}

final PriceFloorRules floors = resolveFloors(account, bidRequest, errors);
final PriceFloorRules floors = resolveFloors(account, bidRequest, warnings);
return updateBidRequestWithFloors(bidRequest, bidder, floors, errors, warnings);
}

Expand Down Expand Up @@ -122,49 +140,13 @@ private static PriceFloorRules extractRequestFloors(BidRequest bidRequest) {
return ObjectUtil.getIfNotNull(prebid, ExtRequestPrebid::getFloors);
}

private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List<String> errors) {
private PriceFloorRules resolveFloors(Account account, BidRequest bidRequest, List<String> warnings) {
final PriceFloorRules requestFloors = extractRequestFloors(bidRequest);

final FetchResult fetchResult = floorFetcher.fetch(account);
final FetchStatus fetchStatus = ObjectUtil.getIfNotNull(fetchResult, FetchResult::getFetchStatus);

if (fetchResult != null && fetchStatus == FetchStatus.success && shouldUseDynamicData(account, fetchResult)) {
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
}
final FetchStatus fetchStatus = fetchResult.getFetchStatus();

if (requestFloors != null) {
try {
final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.of(account)
.map(Account::getAuction)
.map(AccountAuctionConfig::getPriceFloors);

final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
.orElse(null);
final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
.orElse(null);

PriceFloorRulesValidator.validateRules(
requestFloors,
PriceFloorsConfigResolver.resolveMaxValue(maxRules),
PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));

return createFloorsFrom(requestFloors, fetchStatus, PriceFloorLocation.request);
} catch (PreBidException e) {
errors.add("Failed to parse price floors from request, with a reason: %s".formatted(e.getMessage()));
conditionalLogger.error(
"Failed to parse price floors from request with id: '%s', with a reason: %s"
.formatted(bidRequest.getId(), e.getMessage()),
0.01d);
}
}

return createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData);
}

private static boolean shouldUseDynamicData(Account account, FetchResult fetchResult) {
final boolean isUsingDynamicDataAllowed = Optional.of(account)
.map(Account::getAuction)
final boolean isUsingDynamicDataAllowed = Optional.ofNullable(account.getAuction())
.map(AccountAuctionConfig::getPriceFloors)
.map(AccountPriceFloorsConfig::getUseDynamicData)
.map(BooleanUtils::isNotFalse)
Expand All @@ -175,12 +157,68 @@ private static boolean shouldUseDynamicData(Account account, FetchResult fetchRe
.map(rate -> ThreadLocalRandom.current().nextInt(USE_FETCH_DATA_RATE_MAX) < rate)
.orElse(true);

return isUsingDynamicDataAllowed && shouldUseDynamicData;
if (fetchStatus == FetchStatus.success && isUsingDynamicDataAllowed && shouldUseDynamicData) {
final PriceFloorRules mergedFloors = mergeFloors(requestFloors, fetchResult.getRulesData());
return createFloorsFrom(mergedFloors, fetchStatus, PriceFloorLocation.fetch);
}

return requestFloors == null
? createFloorsFrom(null, fetchStatus, PriceFloorLocation.noData)
: getPriceFloorRules(
bidRequest, account, requestFloors, fetchResult, isUsingDynamicDataAllowed, warnings);
}

private PriceFloorRules getPriceFloorRules(BidRequest bidRequest,
Account account,
PriceFloorRules requestFloors,
FetchResult fetchResult,
boolean isDynamicDataAllowed,
List<String> warnings) {

try {
final Optional<AccountPriceFloorsConfig> priceFloorsConfig = Optional.of(account.getAuction())
.map(AccountAuctionConfig::getPriceFloors);

final Long maxRules = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxRules)
.orElse(null);
final Long maxDimensions = priceFloorsConfig.map(AccountPriceFloorsConfig::getMaxSchemaDims)
.orElse(null);

PriceFloorRulesValidator.validateRules(
requestFloors,
PriceFloorsConfigResolver.resolveMaxValue(maxRules),
PriceFloorsConfigResolver.resolveMaxValue(maxDimensions));

return createFloorsFrom(requestFloors, fetchResult.getFetchStatus(), PriceFloorLocation.request);
} catch (PreBidException e) {
logErrorMessage(fetchResult, isDynamicDataAllowed, e, account.getId(), bidRequest.getId(), warnings);
return createFloorsFrom(null, fetchResult.getFetchStatus(), PriceFloorLocation.noData);
}
}

private PriceFloorRules mergeFloors(PriceFloorRules requestFloors,
PriceFloorData providerRulesData) {
private void logErrorMessage(FetchResult fetchResult,
boolean isDynamicDataAllowed,
PreBidException requestFloorsValidationException,
String accountId,
String requestId,
List<String> warnings) {

final String validationMessage = requestFloorsValidationException.getMessage();
final String errorMessage = switch (fetchResult.getFetchStatus()) {
case inprogress -> null;
case error, timeout, none -> FETCH_FAILED_ERROR_MESSAGE.formatted(
fetchResult.getErrorMessage(), validationMessage);
case success -> isDynamicDataAllowed ? null : DYNAMIC_DATA_NOT_ALLOWED_MESSAGE.formatted(validationMessage);
};

if (errorMessage != null) {
warnings.add(INVALID_REQUEST_WARNING_MESSAGE.formatted(validationMessage));
conditionalLogger.error(ERROR_LOG_MESSAGE.formatted(accountId, requestId, errorMessage), logSamplingRate);
metrics.updateAlertsMetrics(MetricName.general);
}
}

private PriceFloorRules mergeFloors(PriceFloorRules requestFloors, PriceFloorData providerRulesData) {
final Price floorMinPrice = resolveFloorMinPrice(requestFloors);

return (requestFloors != null ? requestFloors.toBuilder() : PriceFloorRules.builder())
Expand Down
Loading
Loading