diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml
index e202d7cfdd6..4853322dc0b 100644
--- a/extra/modules/optable-targeting/pom.xml
+++ b/extra/modules/optable-targeting/pom.xml
@@ -12,4 +12,12 @@
optable-targeting
Optable targeting module
+
+
+
+ io.vertx
+ vertx-junit5
+ test
+
+
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java
index 91afba88a31..9f28975a03d 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java
@@ -1,15 +1,24 @@
package org.prebid.server.hooks.modules.optable.targeting.config;
import org.apache.commons.lang3.ObjectUtils;
+import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
+import org.prebid.server.bidder.BidderCatalog;
import org.prebid.server.cache.PbcStorageService;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook;
import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook;
import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule;
import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.AliasesResolver;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting;
import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl;
import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient;
@@ -46,12 +55,12 @@ APIClientImpl apiClient(HttpClient httpClient,
@Value("${logging.sampling-rate:0.01}")
double logSamplingRate,
OptableTargetingProperties optableTargetingProperties,
- JacksonMapper jacksonMapperr) {
+ JacksonMapper jacksonMapper) {
return new APIClientImpl(
optableTargetingProperties.getApiEndpoint(),
httpClient,
- jacksonMapperr,
+ jacksonMapper,
logSamplingRate);
}
@@ -86,19 +95,38 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties
return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties);
}
+ @Bean
+ NetworkCall networkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) {
+ return new NetworkCall(optableTargeting, userFpdActivityMask);
+ }
+
@Bean
OptableTargetingModule optableTargetingModule(ConfigResolver configResolver,
- OptableTargeting optableTargeting,
- UserFpdActivityMask userFpdActivityMask,
+ NetworkCall networkCall,
JsonMerger jsonMerger,
+ BidderCatalog bidderCatalog,
+ JacksonMapper mapper,
+ @Value("${hooks.host-execution-plan:}")
+ String executionPlan,
@Value("${logging.sampling-rate:0.01}") double logSamplingRate) {
+ final CompositeHookExecutionPlan hooksExecutionPlan = CompositeHookExecutionPlan.of(
+ StringUtils.isNoneEmpty(executionPlan)
+ ? mapper.decodeValue(executionPlan, ExecutionPlan.class)
+ : null);
+
return new OptableTargetingModule(List.of(
+ new OptableRawAuctionRequestHook(
+ configResolver,
+ networkCall,
+ BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog)),
+ logSamplingRate),
new OptableTargetingProcessedAuctionRequestHook(
configResolver,
- optableTargeting,
- userFpdActivityMask,
+ networkCall,
+ hooksExecutionPlan,
logSamplingRate),
+ new OptableBidderRequestHook(),
new OptableTargetingAuctionResponseHook(
configResolver,
ObjectMapperProvider.mapper(),
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java
index ed0264f0249..5574a7a72d8 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java
@@ -1,10 +1,14 @@
package org.prebid.server.hooks.modules.optable.targeting.model;
+import io.vertx.core.Future;
import lombok.Data;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import java.util.List;
+import java.util.Set;
@Data
public class ModuleContext {
@@ -19,8 +23,29 @@ public class ModuleContext {
private long optableTargetingExecutionTime;
+ private boolean isEarlyNetworkCallEnabled = false;
+
+ private Future optableTargetingCall;
+
+ private long callTargetingAPITimestamp;
+
+ private Set biddersToEnrich;
+
+ private OptableTargetingProperties optableTargetingProperties;
+
+ private boolean shouldSkipEnrichment;
+
public static ModuleContext of(AuctionInvocationContext invocationContext) {
final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext();
return moduleContext != null ? moduleContext : new ModuleContext();
}
+
+ public void failWithExecutionTime(long executionTime) {
+ setOptableTargetingExecutionTime(executionTime);
+ setEnrichRequestStatus(EnrichmentStatus.failure());
+ }
+
+ public boolean hasOptableTargetingProperties() {
+ return optableTargetingProperties != null;
+ }
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java
index 7f0598da83e..4db54642903 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java
@@ -3,6 +3,7 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import lombok.NoArgsConstructor;
+import org.apache.commons.collections.MapUtils;
import java.util.Map;
import java.util.Set;
@@ -42,4 +43,17 @@ public final class OptableTargetingProperties {
Set optableInserterEidsIgnore = Set.of();
CacheProperties cache = new CacheProperties();
+
+ Integer enrichmentPercentage = 100;
+
+ @JsonProperty("bidder-enrichment-percentages")
+ Map bidderEnrichmentPercentages = Map.of();
+
+ Boolean enrichWeb = true;
+
+ Boolean enrichApp = true;
+
+ public boolean isPerBidderEnrichmentEnabled() {
+ return enrichmentPercentage != 100 || MapUtils.isNotEmpty(bidderEnrichmentPercentages);
+ }
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java
new file mode 100644
index 00000000000..467124c1286
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java
@@ -0,0 +1,83 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import io.vertx.core.Future;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderRequestEnricher;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
+import org.prebid.server.hooks.v1.bidder.BidderRequestHook;
+import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
+
+import java.util.Set;
+
+public class OptableBidderRequestHook implements BidderRequestHook {
+
+ public static final String CODE = "optable-targeting-bidder-request-hook";
+
+ @Override
+ public Future> call(BidderRequestPayload bidderRequestPayload,
+ BidderInvocationContext invocationContext) {
+
+ final ModuleContext moduleContext = ModuleContext.of(invocationContext);
+ final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties();
+ if (!properties.isPerBidderEnrichmentEnabled()) {
+ return noAction(moduleContext);
+ }
+
+ final Set biddersToEnrich = moduleContext.getBiddersToEnrich();
+ if (CollectionUtils.isEmpty(biddersToEnrich)) {
+ return noAction(moduleContext);
+ }
+
+ return moduleContext.getOptableTargetingCall()
+ .compose(targetingResult ->
+ enrichedPayload(
+ targetingResult, moduleContext, moduleContext.getOptableTargetingProperties()))
+ .recover(throwable -> noAction(moduleContext));
+ }
+
+ private Future> enrichedPayload(TargetingResult targetingResult,
+ ModuleContext moduleContext,
+ OptableTargetingProperties properties) {
+
+ moduleContext.setTargeting(targetingResult.getAudience());
+ moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
+
+ return update(BidderRequestEnricher.of(targetingResult, properties), moduleContext);
+ }
+
+ private Future> noAction(ModuleContext moduleContext) {
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ private static Future> update(
+ PayloadUpdate payloadUpdate,
+ ModuleContext moduleContext) {
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .payloadUpdate(payloadUpdate)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java
new file mode 100644
index 00000000000..d03982d559e
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java
@@ -0,0 +1,120 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import org.apache.commons.collections4.CollectionUtils;
+import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook;
+import org.prebid.server.log.ConditionalLogger;
+import org.prebid.server.log.LoggerFactory;
+
+import java.util.Objects;
+import java.util.Set;
+
+public class OptableRawAuctionRequestHook implements RawAuctionRequestHook {
+
+ private static final ConditionalLogger conditionalLogger = new ConditionalLogger(
+ LoggerFactory.getLogger(OptableRawAuctionRequestHook.class));
+
+ private static final String CODE = "optable-targeting-raw-auction-request-hook";
+
+ private final ConfigResolver configResolver;
+ private final NetworkCall networkCall;
+ private final BidderEnrichmentSampler bidderEnrichmentSampler;
+ private final double logSamplingRate;
+
+ public OptableRawAuctionRequestHook(ConfigResolver configResolver,
+ NetworkCall networkCall,
+ BidderEnrichmentSampler bidderEnrichmentSampler,
+ double logSamplingRate) {
+
+ this.configResolver = Objects.requireNonNull(configResolver);
+ this.networkCall = Objects.requireNonNull(networkCall);
+ this.bidderEnrichmentSampler = Objects.requireNonNull(bidderEnrichmentSampler);
+ this.logSamplingRate = logSamplingRate;
+ }
+
+ @Override
+ public Future> call(AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext) {
+
+ final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig());
+ final ModuleContext moduleContext = new ModuleContext();
+ moduleContext.setEarlyNetworkCallEnabled(true);
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis());
+ moduleContext.setOptableTargetingProperties(properties);
+
+ if (!PropertiesValidator.isValid(properties)) {
+ conditionalLogger.error(
+ "Account not properly configured: tenant and/or origin is missing.", logSamplingRate);
+
+ moduleContext.failWithExecutionTime(
+ System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp());
+
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest();
+ if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties)) {
+ moduleContext.setShouldSkipEnrichment(true);
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ final Set biddersToEnrich = bidderEnrichmentSampler.sample(bidRequest, properties);
+ if (CollectionUtils.isEmpty(biddersToEnrich)) {
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ moduleContext.setBiddersToEnrich(biddersToEnrich);
+ final Future optableTargetingCall = networkCall.makeRequest(
+ payload,
+ invocationContext,
+ properties);
+
+ moduleContext.setOptableTargetingCall(optableTargetingCall);
+
+ return updateModuleContext(moduleContext);
+ }
+
+ private static Future> updateModuleContext(ModuleContext moduleContext) {
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.no_action)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ public static Future> update(
+ PayloadUpdate payloadUpdate,
+ ModuleContext moduleContext) {
+
+ return Future.succeededFuture(
+ InvocationResultImpl.builder()
+ .status(InvocationStatus.success)
+ .action(InvocationAction.update)
+ .payloadUpdate(payloadUpdate)
+ .moduleContext(moduleContext)
+ .build());
+ }
+
+ @Override
+ public String code() {
+ return CODE;
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java
index 5a20f79a347..8edef6b00eb 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java
@@ -52,7 +52,7 @@ public Future> call(AuctionResponsePayl
final ModuleContext moduleContext = ModuleContext.of(invocationContext);
moduleContext.setAdserverTargetingEnabled(adserverTargeting);
- if (!adserverTargeting) {
+ if (moduleContext.isShouldSkipEnrichment() || !adserverTargeting) {
return success(moduleContext);
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java
index a5ad2559d40..64fa23fc39e 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java
@@ -1,31 +1,18 @@
package org.prebid.server.hooks.modules.optable.targeting.v1;
-import com.iab.openrtb.request.BidRequest;
-import com.iab.openrtb.request.Device;
-import com.iab.openrtb.request.User;
import io.vertx.core.Future;
-import org.apache.commons.lang3.StringUtils;
-import org.prebid.server.activity.Activity;
-import org.prebid.server.activity.ComponentType;
-import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
-import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload;
-import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl;
-import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload;
-import org.prebid.server.auction.model.AuctionContext;
-import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
-import org.prebid.server.execution.timeout.Timeout;
import org.prebid.server.hooks.execution.v1.InvocationResultImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
-import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
-import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver;
-import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
@@ -35,6 +22,7 @@
import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook;
import org.prebid.server.log.ConditionalLogger;
import org.prebid.server.log.LoggerFactory;
+import org.prebid.server.settings.model.Account;
import java.util.Objects;
@@ -45,19 +33,23 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc
public static final String CODE = "optable-targeting-processed-auction-request-hook";
+ private static final String AUCTION_NOT_PROPERLY_CONFIGURED =
+ "Account not properly configured: tenant and/or origin is missing.";
+
private final ConfigResolver configResolver;
- private final OptableTargeting optableTargeting;
- private final UserFpdActivityMask userFpdActivityMask;
+ private final NetworkCall networkCall;
private final double logSamplingRate;
+ private final CompositeHookExecutionPlan hooksExecutionPlan;
+
public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver,
- OptableTargeting optableTargeting,
- UserFpdActivityMask userFpdActivityMask,
+ NetworkCall networkCall,
+ CompositeHookExecutionPlan hooksExecutionPlan,
double logSamplingRate) {
this.configResolver = Objects.requireNonNull(configResolver);
- this.optableTargeting = Objects.requireNonNull(optableTargeting);
- this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask);
+ this.networkCall = Objects.requireNonNull(networkCall);
+ this.hooksExecutionPlan = hooksExecutionPlan;
this.logSamplingRate = logSamplingRate;
}
@@ -65,94 +57,93 @@ public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver
public Future> call(AuctionRequestPayload auctionRequestPayload,
AuctionInvocationContext invocationContext) {
- final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig());
- final ModuleContext moduleContext = new ModuleContext();
- final long callTargetingAPITimestamp = System.currentTimeMillis();
-
- if (!isTargetingPropertiesValid(properties)) {
- conditionalLogger.error(
- "Account not properly configured: tenant and/or origin is missing.", logSamplingRate);
-
- moduleContext.setOptableTargetingExecutionTime(System.currentTimeMillis() - callTargetingAPITimestamp);
- moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure());
+ final ModuleContext moduleContext = ModuleContext.of(invocationContext);
+ if (moduleContext.isShouldSkipEnrichment()) {
+ moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext));
return update(BidRequestCleaner.instance(), moduleContext);
}
- final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext);
+ final Account account = invocationContext.auctionContext().getAccount();
+ final boolean hasRawAuctionRequestHook = hooksExecutionPlan.hasRawAuctionRequestHook(account);
+ final boolean hasBidderRequestHook = hooksExecutionPlan.hasBidderRequestHook(account);
+
+ final OptableTargetingProperties properties =
+ resolveOptableTargetingProperties(moduleContext, invocationContext);
- final Timeout timeout = getHookTimeout(invocationContext);
- final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes(
- invocationContext.auctionContext(),
- properties.getTimeout());
+ final Future optableTargetingCall = hasRawAuctionRequestHook
+ ? resolveEarlyNetworkCall(moduleContext)
+ : resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties);
- return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout)
+ if (optableTargetingCall == null) {
+ moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext));
+ return update(BidRequestCleaner.instance(), moduleContext);
+ }
+
+ return optableTargetingCall
.compose(targetingResult -> {
- moduleContext.setOptableTargetingExecutionTime(
- System.currentTimeMillis() - callTargetingAPITimestamp);
- return enrichedPayload(targetingResult, moduleContext, properties);
+ moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext));
+ return enrichPayload(hasBidderRequestHook, targetingResult, moduleContext, properties);
})
.recover(throwable -> {
- moduleContext.setOptableTargetingExecutionTime(
- System.currentTimeMillis() - callTargetingAPITimestamp);
- moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure());
+ moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext));
return update(BidRequestCleaner.instance(), moduleContext);
});
}
- private boolean isTargetingPropertiesValid(OptableTargetingProperties properties) {
- return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant());
- }
+ private Future> enrichPayload(
+ boolean perBidderEnrichmentEnabled,
+ TargetingResult targetingResult,
+ ModuleContext moduleContext,
+ OptableTargetingProperties properties) {
- private BidRequest applyActivityRestrictions(BidRequest bidRequest,
- AuctionInvocationContext auctionInvocationContext) {
+ moduleContext.setTargeting(targetingResult.getAudience());
+ moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
- final AuctionContext auctionContext = auctionInvocationContext.auctionContext();
- final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of(
- ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE),
- bidRequest);
- final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure();
+ final PayloadUpdate payloadUpdate = perBidderEnrichmentEnabled
+ ? BidRequestCleaner.instance()
+ : BidRequestCleaner.instance().andThen(BidRequestEnricher.of(targetingResult, properties))::apply;
- final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed(
- Activity.TRANSMIT_UFPD, activityInvocationPayload);
- final boolean disallowTransmitEids = !activityInfrastructure.isAllowed(
- Activity.TRANSMIT_EIDS, activityInvocationPayload);
- final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed(
- Activity.TRANSMIT_GEO, activityInvocationPayload);
+ return update(payloadUpdate, moduleContext);
+ }
- return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo);
+ private Future resolveEarlyNetworkCall(ModuleContext moduleContext) {
+ return moduleContext.getOptableTargetingCall();
}
- private BidRequest maskUserPersonalInfo(BidRequest bidRequest,
- boolean disallowTransmitUfpd,
- boolean disallowTransmitEids,
- boolean disallowTransmitGeo) {
+ private static long calcAPICallExecutionTime(ModuleContext moduleContext) {
+ return System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp();
+ }
- final User maskedUser = userFpdActivityMask.maskUser(
- bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids);
- final Device maskedDevice = userFpdActivityMask.maskDevice(
- bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo);
+ private OptableTargetingProperties resolveOptableTargetingProperties(ModuleContext moduleContext,
+ AuctionInvocationContext invocationContext) {
- return bidRequest.toBuilder()
- .user(maskedUser)
- .device(maskedDevice)
- .build();
- }
+ final OptableTargetingProperties properties = moduleContext.hasOptableTargetingProperties()
+ ? moduleContext.getOptableTargetingProperties()
+ : configResolver.resolve(invocationContext.accountConfig());
+ moduleContext.setOptableTargetingProperties(properties);
- private Timeout getHookTimeout(AuctionInvocationContext invocationContext) {
- return invocationContext.timeout();
+ return properties;
}
- private Future> enrichedPayload(TargetingResult targetingResult,
- ModuleContext moduleContext,
- OptableTargetingProperties properties) {
+ private Future resolvePreEarlyNetworkCall(
+ AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext,
+ ModuleContext moduleContext,
+ OptableTargetingProperties properties) {
- moduleContext.setTargeting(targetingResult.getAudience());
- moduleContext.setEnrichRequestStatus(EnrichmentStatus.success());
- return update(
- BidRequestCleaner.instance()
- .andThen(BidRequestEnricher.of(targetingResult, properties))
- ::apply,
- moduleContext);
+ moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis());
+ if (!PropertiesValidator.isValid(properties)) {
+ conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate);
+
+ moduleContext.failWithExecutionTime(
+ System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp());
+ return Future.failedFuture(AUCTION_NOT_PROPERLY_CONFIGURED);
+ }
+
+ return networkCall.makeRequest(
+ payload,
+ invocationContext,
+ properties);
}
private static Future> update(
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java
new file mode 100644
index 00000000000..f86724de969
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java
@@ -0,0 +1,27 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import lombok.AllArgsConstructor;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.util.PbsUtil;
+
+import java.util.Map;
+import java.util.Optional;
+
+@AllArgsConstructor(staticName = "of")
+public class AliasesResolver {
+
+ private final BidderCatalog bidderCatalog;
+
+ public BidderAliases resolve(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest)
+ .map(PbsUtil::extRequestPrebid)
+ .map(extRequestPrebid -> {
+ final Map aliases = extRequestPrebid.getAliases();
+ final Map aliasesGvlIds = extRequestPrebid.getAliasgvlids();
+ return BidderAliases.of(aliases, aliasesGvlIds, bidderCatalog);
+ })
+ .orElseGet(() -> BidderAliases.of(Map.of(), Map.of(), bidderCatalog));
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java
index 2a60389802c..81c82b55580 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java
@@ -1,39 +1,15 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
-import com.iab.openrtb.request.BidRequest;
-import com.iab.openrtb.request.Data;
-import com.iab.openrtb.request.Eid;
-import com.iab.openrtb.request.Segment;
-import com.iab.openrtb.request.Uid;
-import org.apache.commons.collections4.CollectionUtils;
-import org.apache.commons.lang3.StringUtils;
import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
-import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
-import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User;
import org.prebid.server.hooks.v1.PayloadUpdate;
import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Optional;
-import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
-import java.util.stream.Stream;
-
-public class BidRequestEnricher implements PayloadUpdate {
-
- private static final String OPTABLE_CO_INSERTER = "optable.co";
-
- private final TargetingResult targetingResult;
- private final OptableTargetingProperties targetingProperties;
+public class BidRequestEnricher extends RequestEnricher implements PayloadUpdate {
private BidRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) {
- this.targetingResult = targetingResult;
- this.targetingProperties = targetingProperties;
+ super(targetingResult, targetingProperties);
}
public static BidRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) {
@@ -44,160 +20,4 @@ public static BidRequestEnricher of(TargetingResult targetingResult, OptableTarg
public AuctionRequestPayload apply(AuctionRequestPayload payload) {
return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest()));
}
-
- private BidRequest enrichBidRequest(BidRequest bidRequest) {
- if (bidRequest == null || targetingResult == null) {
- return bidRequest;
- }
-
- final User optableUser = Optional.of(targetingResult)
- .map(TargetingResult::getOrtb2)
- .map(Ortb2::getUser)
- .orElse(null);
-
- if (optableUser == null) {
- return bidRequest;
- }
-
- final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser())
- .orElseGet(() -> com.iab.openrtb.request.User.builder().build());
-
- return bidRequest.toBuilder()
- .user(mergeUserData(bidRequestUser, optableUser))
- .build();
- }
-
- private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) {
- return user.toBuilder()
- .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids())))
- .data(mergeData(user.getData(), optableUser.getData()))
- .build();
- }
-
- private List mergeEids(List destination, List source) {
- if (CollectionUtils.isEmpty(destination)) {
- return source;
- }
-
- if (CollectionUtils.isEmpty(source)) {
- return destination;
- }
-
- final Map idToSourceEid = source.stream().collect(Collectors.toMap(
- BidRequestEnricher::eidIdExtractor,
- Function.identity(),
- (a, b) -> b,
- HashMap::new));
-
- final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace();
- final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge()
- .stream()
- .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet());
-
- final List mergedEid = destination.stream()
- .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid))
- && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter())
- ? resolveEidConflict(
- destinationEid,
- idToSourceEid.get(eidIdExtractor(destinationEid)),
- sourceToMerge,
- sourceToReplace)
- : destinationEid)
- .toList();
-
- return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor);
- }
-
- private List filterOptableEids(List eids) {
- if (CollectionUtils.isEmpty(eids)) {
- return eids;
- }
-
- final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore();
- if (CollectionUtils.isEmpty(optableIdsToIgnore)) {
- return eids;
- }
-
- return eids.stream()
- .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter())
- || !optableIdsToIgnore.contains(eid.getSource()))
- .toList();
- }
-
- private static Eid resolveEidConflict(Eid destinationEid,
- Eid sourceEid,
- Set sourceToMerge,
- Set sourceToReplace) {
-
- final String eidSource = sourceEid.getSource();
-
- if (sourceToReplace.contains(eidSource)) {
- return sourceEid;
- }
- if (sourceToMerge.contains(eidSource)) {
- return mergeEid(destinationEid, sourceEid);
- }
-
- return destinationEid;
- }
-
- private static Eid mergeEid(Eid destinationEid, Eid sourceEid) {
- return destinationEid.toBuilder()
- .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId))
- .build();
- }
-
- private static String eidIdExtractor(Eid eid) {
- return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource());
- }
-
- private static List mergeData(List destination, List source) {
- if (CollectionUtils.isEmpty(destination)) {
- return source;
- }
-
- if (CollectionUtils.isEmpty(source)) {
- return destination;
- }
-
- final Map idToSourceData = source.stream()
- .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new));
-
- final List mergedData = destination.stream()
- .map(destinationData -> idToSourceData.containsKey(destinationData.getId())
- ? mergeData(destinationData, idToSourceData.get(destinationData.getId()))
- : destinationData)
- .toList();
-
- return merge(mergedData, source, Data::getId);
- }
-
- private static Data mergeData(Data destinationData, Data sourceData) {
- return destinationData.toBuilder()
- .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId))
- .build();
- }
-
- private static List merge(List destination,
- List source,
- Function idExtractor) {
-
- if (CollectionUtils.isEmpty(source)) {
- return destination;
- }
-
- if (CollectionUtils.isEmpty(destination)) {
- return source;
- }
-
- final Set existingIds = destination.stream()
- .map(idExtractor)
- .collect(Collectors.toSet());
-
- return Stream.concat(
- destination.stream(),
- source.stream()
- .filter(entry -> !existingIds.contains(idExtractor.apply(entry))))
- .toList();
- }
}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java
new file mode 100644
index 00000000000..95fe7820176
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java
@@ -0,0 +1,65 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import lombok.AllArgsConstructor;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.util.StreamUtil;
+
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.Set;
+import java.util.concurrent.ThreadLocalRandom;
+import java.util.function.IntSupplier;
+import java.util.stream.Collectors;
+
+@AllArgsConstructor(staticName = "of")
+public class BidderEnrichmentSampler {
+
+ private final AliasesResolver aliasesResolver;
+ private final IntSupplier randomSupplier;
+
+ public static BidderEnrichmentSampler of(AliasesResolver aliasesResolver) {
+ return of(aliasesResolver, () -> ThreadLocalRandom.current().nextInt(100));
+ }
+
+ public Set sample(BidRequest bidRequest, OptableTargetingProperties optableTargetingProperties) {
+ final Integer defaultEnrichmentPercentage = optableTargetingProperties.getEnrichmentPercentage();
+ final Map bidderEnrichmentPercentage =
+ optableTargetingProperties.getBidderEnrichmentPercentages();
+
+ final BidderAliases aliases = aliasesResolver.resolve(bidRequest);
+ return extractUniqueBidders(bidRequest)
+ .stream()
+ .filter(bidder -> {
+ final int percentage =
+ resolvePercentage(aliases, bidder, defaultEnrichmentPercentage, bidderEnrichmentPercentage);
+ return randomSupplier.getAsInt() <= percentage;
+ })
+ .collect(Collectors.toSet());
+ }
+
+ private static int resolvePercentage(BidderAliases aliases, String bidder,
+ Integer defaultEnrichmentPercentage,
+ Map bidderEnrichmentPercentage) {
+
+ return Optional.ofNullable(bidderEnrichmentPercentage.get(bidder))
+ .or(() -> Optional.ofNullable(bidderEnrichmentPercentage.get(aliases.resolveBidder(bidder))))
+ .orElse(defaultEnrichmentPercentage);
+ }
+
+ private static Set extractUniqueBidders(BidRequest bidRequest) {
+ return Optional.ofNullable(bidRequest.getImp())
+ .stream()
+ .flatMap(Collection::stream)
+ .map(Imp::getExt)
+ .filter(Objects::nonNull)
+ .map(ext -> ext.at("/prebid/bidder"))
+ .filter(Objects::nonNull)
+ .flatMap(bidder -> StreamUtil.asStream(bidder.fieldNames()))
+ .collect(Collectors.toSet());
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java
new file mode 100644
index 00000000000..24b59cd42a1
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java
@@ -0,0 +1,23 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.v1.PayloadUpdate;
+import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
+
+public class BidderRequestEnricher extends RequestEnricher implements PayloadUpdate {
+
+ private BidderRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) {
+ super(targetingResult, targetingProperties);
+ }
+
+ public static BidderRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) {
+ return new BidderRequestEnricher(targetingResult, properties);
+ }
+
+ @Override
+ public BidderRequestPayload apply(BidderRequestPayload payload) {
+ return BidderRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest()));
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java
new file mode 100644
index 00000000000..ffd9b0b00a8
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java
@@ -0,0 +1,90 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
+import org.prebid.server.hooks.execution.model.ExecutionGroup;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionPlan;
+import org.prebid.server.model.Endpoint;
+import org.prebid.server.settings.model.Account;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class CompositeHookExecutionPlan {
+
+ private static final String ENDPOINT_AUCTION = "openrtb2_auction";
+ private static final String STAGE_RAW_AUCTION_REQUEST = "raw_auction_request";
+ private static final String STAGE_BIDDER_REQUEST = "bidder_request";
+ private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = "optable-targeting-raw-auction-request-hook";
+ private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = "optable-targeting-bidder-request-hook";
+
+ private final boolean hasGlobalRawAuctionRequestHook;
+
+ private final boolean hasGlobalBidderRequestHook;
+
+ private final ConcurrentHashMap rawAuctionRequestHookCache = new ConcurrentHashMap<>();
+ private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>();
+
+ private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook, boolean hasGlobalBidderRequestHook) {
+ this.hasGlobalRawAuctionRequestHook = hasGlobalRawAuctionRequestHook;
+ this.hasGlobalBidderRequestHook = hasGlobalBidderRequestHook;
+ }
+
+ public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) {
+ return globalExecutionPlan == null
+ ? new CompositeHookExecutionPlan(false, false)
+ : new CompositeHookExecutionPlan(
+ hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION),
+ hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST));
+ }
+
+ public boolean hasRawAuctionRequestHook(Account account) {
+ final String accountId = account != null ? account.getId() : null;
+
+ return StringUtils.isNotEmpty(accountId)
+ ? rawAuctionRequestHookCache.computeIfAbsent(accountId, id -> {
+ final ExecutionPlan accountSpecificHooksExecutionPlan = resolveExecutionPlan(account);
+ return hasHook(
+ accountSpecificHooksExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION)
+ || hasGlobalRawAuctionRequestHook;
+ })
+ : false;
+ }
+
+ public boolean hasBidderRequestHook(Account account) {
+ final String accountId = account != null ? account.getId() : null;
+
+ return StringUtils.isNotEmpty(accountId)
+ ? bidderRequestHookCache.computeIfAbsent(accountId, id -> {
+ final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account);
+ return hasHook(
+ accountSpecificHoksExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)
+ || hasGlobalBidderRequestHook;
+ })
+ : false;
+ }
+
+ private ExecutionPlan resolveExecutionPlan(Account account) {
+ return Optional.ofNullable(account)
+ .map(org.prebid.server.settings.model.Account::getHooks)
+ .map(org.prebid.server.settings.model.AccountHooksConfiguration::getExecutionPlan)
+ .orElse(null);
+ }
+
+ private static boolean hasHook(ExecutionPlan executionPlan, String stage, String hookCode) {
+ return Optional.ofNullable(executionPlan)
+ .map(ExecutionPlan::getEndpoints)
+ .map(endpoints -> endpoints.get(Endpoint.valueOf(ENDPOINT_AUCTION)))
+ .map(EndpointExecutionPlan::getStages)
+ .map(stages -> stages.get(Stage.valueOf(stage)))
+ .map(StageExecutionPlan::getGroups)
+ .orElseGet(List::of)
+ .stream()
+ .map(ExecutionGroup::getHookSequence)
+ .flatMap(java.util.Collection::stream)
+ .anyMatch(hook -> hookCode.equals(hook.getHookImplCode()));
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java
new file mode 100644
index 00000000000..be7d2c42acb
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java
@@ -0,0 +1,88 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.User;
+import io.vertx.core.Future;
+import org.prebid.server.activity.Activity;
+import org.prebid.server.activity.ComponentType;
+import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
+import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload;
+import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl;
+import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload;
+import org.prebid.server.auction.model.AuctionContext;
+import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+
+import java.util.Objects;
+
+public class NetworkCall {
+
+ private final OptableTargeting optableTargeting;
+ private final UserFpdActivityMask userFpdActivityMask;
+
+ public NetworkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) {
+
+ this.optableTargeting = Objects.requireNonNull(optableTargeting);
+ this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask);
+ }
+
+ public Future makeRequest(AuctionRequestPayload payload,
+ AuctionInvocationContext invocationContext,
+ OptableTargetingProperties properties) {
+
+ final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext);
+
+ final Timeout timeout = getHookTimeout(invocationContext);
+ final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes(
+ invocationContext.auctionContext(),
+ properties.getTimeout());
+
+ return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout);
+ }
+
+ private static Timeout getHookTimeout(AuctionInvocationContext invocationContext) {
+ return invocationContext.timeout();
+ }
+
+ private BidRequest applyActivityRestrictions(BidRequest bidRequest,
+ AuctionInvocationContext auctionInvocationContext) {
+
+ final AuctionContext auctionContext = auctionInvocationContext.auctionContext();
+ final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of(
+ ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE),
+ bidRequest);
+ final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure();
+
+ final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed(
+ Activity.TRANSMIT_UFPD, activityInvocationPayload);
+ final boolean disallowTransmitEids = !activityInfrastructure.isAllowed(
+ Activity.TRANSMIT_EIDS, activityInvocationPayload);
+ final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed(
+ Activity.TRANSMIT_GEO, activityInvocationPayload);
+
+ return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo);
+ }
+
+ private BidRequest maskUserPersonalInfo(BidRequest bidRequest,
+ boolean disallowTransmitUfpd,
+ boolean disallowTransmitEids,
+ boolean disallowTransmitGeo) {
+
+ final User maskedUser = userFpdActivityMask.maskUser(
+ bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids);
+ final Device maskedDevice = userFpdActivityMask.maskDevice(
+ bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo);
+
+ return bidRequest.toBuilder()
+ .user(maskedUser)
+ .device(maskedDevice)
+ .build();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java
index 7f2aad0657d..8009a9f2ff5 100644
--- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java
@@ -1,11 +1,16 @@
package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+import com.iab.openrtb.request.BidRequest;
import com.iab.openrtb.request.Device;
+import com.iab.openrtb.request.Regs;
+import com.iab.openrtb.request.User;
import org.apache.commons.collections4.SetUtils;
+import org.apache.commons.lang3.StringUtils;
import org.prebid.server.auction.gpp.model.GppContext;
import org.prebid.server.auction.model.AuctionContext;
import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes;
-import org.prebid.server.privacy.gdpr.model.TcfContext;
+import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
+import org.prebid.server.proto.openrtb.ext.request.ExtUser;
import java.util.ArrayList;
import java.util.List;
@@ -17,18 +22,33 @@ private OptableAttributesResolver() {
}
public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) {
- final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext();
final GppContext.Scope gppScope = auctionContext.getGppContext().scope();
+ final BidRequest bidRequest = auctionContext.getBidRequest();
+ final Optional regs = Optional.ofNullable(bidRequest.getRegs());
+ final Integer gdpr = regs
+ .map(Regs::getGdpr)
+ .orElseGet(() -> regs.map(Regs::getExt)
+ .map(ExtRegs::getGdpr)
+ .orElse(null));
+
final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder()
.ips(resolveIp(auctionContext))
.userAgent(resolveUserAgent(auctionContext))
.timeout(timeout);
- if (tcfContext.isConsentValid()) {
- builder
- .gdprApplies(tcfContext.isInGdprScope())
- .gdprConsent(tcfContext.getConsentString());
+ if (gdpr != null && gdpr > 0) {
+ final Optional user = Optional.ofNullable(bidRequest.getUser());
+ final String consent = user.map(User::getConsent)
+ .orElseGet(() -> user.map(User::getExt)
+ .map(ExtUser::getConsent)
+ .orElse(null));
+
+ if (StringUtils.isNotEmpty(consent)) {
+ builder
+ .gdprApplies(true)
+ .gdprConsent(consent);
+ }
}
if (gppScope.getGppModel() != null) {
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java
new file mode 100644
index 00000000000..391590057ff
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java
@@ -0,0 +1,20 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+
+public class PropertiesValidator {
+
+ private PropertiesValidator() {
+ }
+
+ public static boolean isValid(OptableTargetingProperties properties) {
+ return StringUtils.isNotEmpty(properties.getOrigin()) && StringUtils.isNotEmpty(properties.getTenant());
+ }
+
+ public static boolean isTrafficSourceValid(BidRequest bidRequest, OptableTargetingProperties properties) {
+ return (Boolean.TRUE.equals(properties.getEnrichWeb()) && bidRequest.getSite() != null)
+ || (Boolean.TRUE.equals(properties.getEnrichApp()) && bidRequest.getApp() != null);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java
new file mode 100644
index 00000000000..634fcb000cc
--- /dev/null
+++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java
@@ -0,0 +1,191 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Data;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.Segment;
+import com.iab.openrtb.request.Uid;
+import org.apache.commons.collections4.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult;
+import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+public abstract class RequestEnricher {
+
+ private static final String OPTABLE_CO_INSERTER = "optable.co";
+
+ private final TargetingResult targetingResult;
+ private final OptableTargetingProperties targetingProperties;
+
+ protected RequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) {
+ this.targetingResult = targetingResult;
+ this.targetingProperties = targetingProperties;
+ }
+
+ protected BidRequest enrichBidRequest(BidRequest bidRequest) {
+ if (bidRequest == null || targetingResult == null) {
+ return bidRequest;
+ }
+
+ final User optableUser = Optional.of(targetingResult)
+ .map(TargetingResult::getOrtb2)
+ .map(Ortb2::getUser)
+ .orElse(null);
+
+ if (optableUser == null) {
+ return bidRequest;
+ }
+
+ final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser())
+ .orElseGet(() -> com.iab.openrtb.request.User.builder().build());
+
+ return bidRequest.toBuilder()
+ .user(mergeUserData(bidRequestUser, optableUser))
+ .build();
+ }
+
+ private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) {
+ return user.toBuilder()
+ .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids())))
+ .data(mergeData(user.getData(), optableUser.getData()))
+ .build();
+ }
+
+ private List mergeEids(List destination, List source) {
+ if (CollectionUtils.isEmpty(destination)) {
+ return source;
+ }
+
+ if (CollectionUtils.isEmpty(source)) {
+ return destination;
+ }
+
+ final Map idToSourceEid = source.stream().collect(Collectors.toMap(
+ RequestEnricher::eidIdExtractor,
+ Function.identity(),
+ (a, b) -> b,
+ HashMap::new));
+
+ final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace();
+ final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge()
+ .stream()
+ .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet());
+
+ final List mergedEid = destination.stream()
+ .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid))
+ && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter())
+ ? resolveEidConflict(
+ destinationEid,
+ idToSourceEid.get(eidIdExtractor(destinationEid)),
+ sourceToMerge,
+ sourceToReplace)
+ : destinationEid)
+ .toList();
+
+ return merge(mergedEid, source, RequestEnricher::eidIdExtractor);
+ }
+
+ private List filterOptableEids(List eids) {
+ if (CollectionUtils.isEmpty(eids)) {
+ return eids;
+ }
+
+ final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore();
+ if (CollectionUtils.isEmpty(optableIdsToIgnore)) {
+ return eids;
+ }
+
+ return eids.stream()
+ .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter())
+ || !optableIdsToIgnore.contains(eid.getSource()))
+ .toList();
+ }
+
+ private static Eid resolveEidConflict(Eid destinationEid,
+ Eid sourceEid,
+ Set sourceToMerge,
+ Set sourceToReplace) {
+
+ final String eidSource = sourceEid.getSource();
+
+ if (sourceToReplace.contains(eidSource)) {
+ return sourceEid;
+ }
+ if (sourceToMerge.contains(eidSource)) {
+ return mergeEid(destinationEid, sourceEid);
+ }
+
+ return destinationEid;
+ }
+
+ private static Eid mergeEid(Eid destinationEid, Eid sourceEid) {
+ return destinationEid.toBuilder()
+ .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId))
+ .build();
+ }
+
+ private static String eidIdExtractor(Eid eid) {
+ return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource());
+ }
+
+ private static List mergeData(List destination, List source) {
+ if (CollectionUtils.isEmpty(destination)) {
+ return source;
+ }
+
+ if (CollectionUtils.isEmpty(source)) {
+ return destination;
+ }
+
+ final Map idToSourceData = source.stream()
+ .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new));
+
+ final List mergedData = destination.stream()
+ .map(destinationData -> idToSourceData.containsKey(destinationData.getId())
+ ? mergeData(destinationData, idToSourceData.get(destinationData.getId()))
+ : destinationData)
+ .toList();
+
+ return merge(mergedData, source, Data::getId);
+ }
+
+ private static Data mergeData(Data destinationData, Data sourceData) {
+ return destinationData.toBuilder()
+ .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId))
+ .build();
+ }
+
+ private static List merge(List destination,
+ List source,
+ Function idExtractor) {
+
+ if (CollectionUtils.isEmpty(source)) {
+ return destination;
+ }
+
+ if (CollectionUtils.isEmpty(destination)) {
+ return source;
+ }
+
+ final Set existingIds = destination.stream()
+ .map(idExtractor)
+ .collect(Collectors.toSet());
+
+ return Stream.concat(
+ destination.stream(),
+ source.stream()
+ .filter(entry -> !existingIds.contains(idExtractor.apply(entry))))
+ .toList();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java
index 99f24ea4bc5..159927e8f1d 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java
@@ -10,6 +10,7 @@
import com.iab.openrtb.request.Eid;
import com.iab.openrtb.request.Geo;
import com.iab.openrtb.request.Segment;
+import com.iab.openrtb.request.Site;
import com.iab.openrtb.request.Uid;
import com.iab.openrtb.request.User;
import com.iab.openrtb.response.Bid;
@@ -40,6 +41,7 @@
import org.prebid.server.privacy.model.Privacy;
import org.prebid.server.privacy.model.PrivacyContext;
import org.prebid.server.proto.openrtb.ext.request.ExtUser;
+import org.prebid.server.settings.model.Account;
import org.prebid.server.vertx.httpclient.model.HttpClientResponse;
import java.io.IOException;
@@ -71,7 +73,9 @@ protected ModuleContext givenModuleContext(List audiences) {
return moduleContext;
}
- protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) {
+ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure,
+ Timeout timeout,
+ Account account) {
final GppModel gppModel = new GppModel();
final TcfContext tcfContext = TcfContext.builder().build();
final GppContext gppContext = new GppContext(
@@ -80,6 +84,7 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr
return AuctionContext.builder()
.bidRequest(givenBidRequest())
+ .account(account)
.activityInfrastructure(activityInfrastructure)
.privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8"))
.gppContext(gppContext)
@@ -87,18 +92,32 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr
.build();
}
+ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) {
+ return givenAuctionContext(activityInfrastructure, timeout, null);
+ }
+
protected BidRequest givenBidRequest() {
return givenBidRequestWithUserEids(null);
}
protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) {
- return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build();
+ return bidRequestCustomizer.apply(BidRequest.builder().id("requestId").site(Site.builder().build())).build();
}
protected BidRequest givenBidRequestWithUserEids(List eids) {
return BidRequest.builder()
.user(givenUser(eids))
.device(givenDevice())
+ .site(Site.builder().build())
+ .cur(List.of("USD"))
+ .build();
+ }
+
+ protected BidRequest givenBidRequestWithUser(User user) {
+ return BidRequest.builder()
+ .user(user)
+ .device(givenDevice())
+ .site(Site.builder().build())
.cur(List.of("USD"))
.build();
}
@@ -107,6 +126,7 @@ protected BidRequest givenBidRequestWithUserData(List data) {
return BidRequest.builder()
.user(givenUserWithData(data))
.device(givenDevice())
+ .site(Site.builder().build())
.cur(List.of("USD"))
.build();
}
@@ -245,6 +265,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key,
optableTargetingProperties.setApiKey(key);
optableTargetingProperties.setPpidMapping(Map.of("c", "id"));
optableTargetingProperties.setAdserverTargeting(true);
+ optableTargetingProperties.setEnrichWeb(true);
optableTargetingProperties.setTimeout(100L);
optableTargetingProperties.setCache(cacheProperties);
@@ -254,4 +275,8 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key,
protected Query givenQuery() {
return Query.of("?que", "ry");
}
+
+ protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) {
+ return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled));
+ }
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java
new file mode 100644
index 00000000000..ac14a7dc3eb
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java
@@ -0,0 +1,196 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.model.Status;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.v1.InvocationAction;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.bidder.BidderInvocationContext;
+import org.prebid.server.hooks.v1.bidder.BidderRequestPayload;
+
+import java.util.Collections;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.when;
+
+@ExtendWith(MockitoExtension.class)
+@MockitoSettings(strictness = Strictness.LENIENT)
+public class OptableBidderRequestHookTest extends BaseOptableTest {
+
+ @Mock
+ private BidderInvocationContext invocationContext;
+
+ @Mock
+ private BidderRequestPayload bidderRequestPayload;
+
+ private OptableBidderRequestHook target;
+
+ @BeforeEach
+ public void setUp() {
+ target = new OptableBidderRequestHook();
+ when(bidderRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ }
+
+ @Test
+ public void shouldHaveRightCode() {
+ // given and when and then
+ assertThat(target.code()).isEqualTo("optable-targeting-bidder-request-hook");
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenOptableTargetingProperties(false));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.moduleContext()).isSameAs(moduleContext);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Collections.emptySet());
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenBiddersToEnrichIsNull() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ }
+
+ @Test
+ public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+
+ final BidRequest enrichedRequest = result
+ .payloadUpdate()
+ .apply(BidderRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(enrichedRequest.getUser().getEids().getFirst().getUids().getFirst().getId())
+ .isEqualTo("id");
+ assertThat(enrichedRequest.getUser().getData().getFirst().getSegment().getFirst().getId())
+ .isEqualTo("id");
+ }
+
+ @Test
+ public void shouldUpdateModuleContextWithTargetingOnSuccess() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(moduleContext.getTargeting()).isNotNull().isNotEmpty();
+ assertThat(moduleContext.getEnrichRequestStatus()).isNotNull()
+ .extracting(EnrichmentStatus::getStatus)
+ .extracting(Status::getValue)
+ .isEqualTo("success");
+ }
+
+ @Test
+ public void shouldReturnNoActionWhenTargetingCallFails() {
+ // given
+ final ModuleContext moduleContext = givenModuleContextWithProperties(
+ givenPropertiesWithPerBidderEnrichmentEnabled());
+ moduleContext.setBiddersToEnrich(Set.of("bidder1"));
+ moduleContext.setOptableTargetingCall(
+ Future.failedFuture(new RuntimeException("targeting service error")));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(bidderRequestPayload, invocationContext);
+
+ // then
+ assertThat(future.succeeded()).isTrue();
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ }
+
+ private static ModuleContext givenModuleContextWithProperties(OptableTargetingProperties properties) {
+ final ModuleContext moduleContext = new ModuleContext();
+ moduleContext.setOptableTargetingProperties(properties);
+ return moduleContext;
+ }
+
+ private OptableTargetingProperties givenPropertiesWithPerBidderEnrichmentEnabled() {
+ final OptableTargetingProperties properties = givenOptableTargetingProperties(false);
+ properties.setEnrichmentPercentage(50);
+ return properties;
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java
new file mode 100644
index 00000000000..f5e6a39663e
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java
@@ -0,0 +1,169 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1;
+
+import com.iab.openrtb.request.BidRequest;
+import io.vertx.core.Future;
+import io.vertx.junit5.VertxExtension;
+import io.vertx.junit5.VertxTestContext;
+import lombok.SneakyThrows;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoSettings;
+import org.mockito.quality.Strictness;
+import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
+import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
+import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting;
+import org.prebid.server.hooks.v1.InvocationResult;
+import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
+import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.Mockito.when;
+
+@MockitoSettings(strictness = Strictness.LENIENT)
+@ExtendWith(VertxExtension.class)
+public class OptableRawAuctionRequestHookTest extends BaseOptableTest {
+
+ @Mock
+ private OptableTargeting optableTargeting;
+ @Mock
+ private UserFpdActivityMask userFpdActivityMask;
+ @Mock
+ private AuctionRequestPayload auctionRequestPayload;
+ @Mock
+ private ActivityInfrastructure activityInfrastructure;
+ @Mock
+ private AuctionInvocationContext invocationContext;
+ @Mock
+ private Timeout timeout;
+ @Mock
+ private BidderEnrichmentSampler bidderEnrichmentSampler;
+
+ private ConfigResolver configResolver;
+ private NetworkCall networkCall;
+ private OptableRawAuctionRequestHook target;
+
+ @BeforeEach
+ public void setUp() {
+ when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean()))
+ .thenAnswer(answer -> answer.getArgument(0));
+ configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false));
+ networkCall = new NetworkCall(optableTargeting, userFpdActivityMask);
+ target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01);
+ when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout));
+ when(invocationContext.timeout()).thenReturn(timeout);
+ when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true);
+ when(timeout.remaining()).thenReturn(1000L);
+ }
+
+ @Test
+ public void shouldHaveRightCode() {
+ // when and then
+ assertThat(target.code()).isEqualTo("optable-targeting-raw-auction-request-hook");
+ }
+
+ @SneakyThrows
+ @Test
+ public void shouldInjectEarlyNetworkCallToModuleContext(VertxTestContext vertxTestContext) {
+ // given
+ when(invocationContext.accountConfig())
+ .thenReturn(givenAccountConfig("key", "tenant", "origin", true));
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+ when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder"));
+
+ // when
+ final Future> result =
+ target.call(auctionRequestPayload, invocationContext);
+
+ // then
+ assertThat(result).isNotNull();
+ result.map(res -> (ModuleContext) res.moduleContext())
+ .compose(ModuleContext::getOptableTargetingCall)
+ .onComplete(call -> {
+ vertxTestContext.verify(() -> {
+ assertThat(call.result()).isNotNull();
+ });
+ vertxTestContext.completeNow();
+ });
+ }
+
+ @SneakyThrows
+ @Test
+ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAccountConfiguration(
+ VertxTestContext vertxTestContext) {
+
+ // given
+ when(invocationContext.accountConfig())
+ .thenReturn(givenAccountConfig("key", "tenant", null, true));
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ configResolver = new ConfigResolver(
+ mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true));
+ target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01);
+
+ // when
+ final Future> result =
+ target.call(auctionRequestPayload, invocationContext);
+
+ // then
+ assertThat(result).isNotNull();
+ result.map(res -> (ModuleContext) res.moduleContext())
+ .onComplete(cxt -> {
+ vertxTestContext.verify(() -> {
+ final ModuleContext moduleContext = cxt.result();
+ assertThat(moduleContext.getOptableTargetingCall()).isNull();
+ assertThat(moduleContext.isEarlyNetworkCallEnabled()).isTrue();
+ });
+ vertxTestContext.completeNow();
+ });
+ }
+
+ @SneakyThrows
+ @Test
+ public void shouldNotInjectEarlyNetworkCallWhenTrafficSourceIsInvalid(VertxTestContext vertxTestContext) {
+ // given
+ when(invocationContext.accountConfig())
+ .thenReturn(givenAccountConfig("key", "tenant", "origin", true));
+ final BidRequest bidRequestWithoutTrafficSource = givenBidRequest(bidRequestCustomizer ->
+ bidRequestCustomizer.site(null).app(null));
+ when(auctionRequestPayload.bidRequest()).thenReturn(bidRequestWithoutTrafficSource);
+ when(invocationContext.auctionContext()).thenReturn(
+ givenAuctionContext(activityInfrastructure, timeout)
+ .toBuilder()
+ .bidRequest(bidRequestWithoutTrafficSource)
+ .build());
+
+ configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false));
+ target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01);
+
+ // when
+ final Future> result =
+ target.call(auctionRequestPayload, invocationContext);
+
+ // then
+ assertThat(result).isNotNull();
+ result.map(res -> (ModuleContext) res.moduleContext())
+ .onComplete(cxt -> {
+ vertxTestContext.verify(() -> {
+ final ModuleContext moduleContext = cxt.result();
+ assertThat(moduleContext.isShouldSkipEnrichment()).isTrue();
+ assertThat(moduleContext.getOptableTargetingCall()).isNull();
+ });
+ vertxTestContext.completeNow();
+ });
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java
index af4a809df78..7d1df51bc5e 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java
@@ -9,12 +9,16 @@
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl;
+import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience;
import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
+import org.prebid.server.hooks.v1.analytics.Activity;
+import org.prebid.server.hooks.v1.analytics.Result;
+import org.prebid.server.hooks.v1.analytics.Tags;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import org.prebid.server.hooks.v1.auction.AuctionResponseHook;
import org.prebid.server.hooks.v1.auction.AuctionResponsePayload;
@@ -67,11 +71,17 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.no_action);
- assertThat(result.analyticsTags().activities().getFirst()
- .results().getFirst().values().get("reason")).isNotNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ assertThat(result.analyticsTags())
+ .extracting(Tags::activities)
+ .extracting(List::getFirst)
+ .extracting(Activity::results)
+ .extracting(List::getFirst)
+ .extracting(Result::values)
+ .extracting(it -> it.get("reason"))
+ .isNotNull();
assertThat(result.errors()).isNull();
}
@@ -139,6 +149,26 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() {
.returns(InvocationAction.no_action, InvocationResult::action);
}
+ @Test
+ public void shouldReturnSuccessWhenSkipEnrichmentIsTrue() {
+ // given
+ final ModuleContext moduleContext = givenModuleContext();
+ moduleContext.setShouldSkipEnrichment(true);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future =
+ target.call(auctionResponsePayload, invocationContext);
+ final InvocationResult result = future.result();
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.no_action, InvocationResult::action);
+ }
+
private ObjectNode givenAccountConfig(boolean cacheEnabled) {
return mapper.valueToTree(givenOptableTargetingProperties(cacheEnabled));
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java
index 008262b8a3e..bd5b8a25d92 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java
@@ -2,27 +2,42 @@
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Data;
+import com.iab.openrtb.request.Eid;
+import com.iab.openrtb.request.Segment;
+import com.iab.openrtb.request.Uid;
import io.vertx.core.Future;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.mockito.junit.jupiter.MockitoSettings;
-import org.mockito.quality.Strictness;
import org.prebid.server.activity.infrastructure.ActivityInfrastructure;
import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask;
import org.prebid.server.execution.timeout.Timeout;
+import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
+import org.prebid.server.hooks.execution.model.ExecutionGroup;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionPlan;
import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl;
import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext;
import org.prebid.server.hooks.modules.optable.targeting.model.Status;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver;
+import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall;
import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting;
import org.prebid.server.hooks.v1.InvocationAction;
import org.prebid.server.hooks.v1.InvocationResult;
import org.prebid.server.hooks.v1.InvocationStatus;
import org.prebid.server.hooks.v1.auction.AuctionInvocationContext;
import org.prebid.server.hooks.v1.auction.AuctionRequestPayload;
+import org.prebid.server.model.Endpoint;
+import org.prebid.server.settings.model.Account;
+
+import java.util.List;
+import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
@@ -30,57 +45,57 @@
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
-@MockitoSettings(strictness = Strictness.LENIENT)
-public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest {
+class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest {
private ConfigResolver configResolver;
@Mock
private OptableTargeting optableTargeting;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private UserFpdActivityMask userFpdActivityMask;
- private OptableTargetingProcessedAuctionRequestHook target;
-
@Mock
private AuctionRequestPayload auctionRequestPayload;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private AuctionInvocationContext invocationContext;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private ActivityInfrastructure activityInfrastructure;
- @Mock
+ @Mock(strictness = Mock.Strictness.LENIENT)
private Timeout timeout;
+ private NetworkCall networkCall;
+
+ private OptableTargetingProcessedAuctionRequestHook target;
+
@BeforeEach
- public void setUp() {
+ void setUp() {
when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean()))
.thenAnswer(answer -> answer.getArgument(0));
configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false));
+ networkCall = new NetworkCall(optableTargeting, userFpdActivityMask);
target = new OptableTargetingProcessedAuctionRequestHook(
- configResolver,
- optableTargeting,
- userFpdActivityMask,
- 0.01);
+ configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true));
- when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout));
+ when(invocationContext.auctionContext()).thenReturn(
+ givenAuctionContext(activityInfrastructure, timeout, Account.builder().id("accountId").build()));
when(invocationContext.timeout()).thenReturn(timeout);
when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true);
when(timeout.remaining()).thenReturn(1000L);
}
@Test
- public void shouldHaveRightCode() {
+ void codeShouldReturnRightCode() {
// when and then
assertThat(target.code()).isEqualTo("optable-targeting-processed-auction-request-hook");
}
@Test
- public void shouldReturnResultWithPBSAnalyticsTags() {
+ void callShouldReturnResultWithPBSAnalyticsTags() {
// given
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any()))
@@ -95,17 +110,130 @@ public void shouldReturnResultWithPBSAnalyticsTags() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
assertThat(result.analyticsTags().activities().getFirst()
.results().getFirst().values().get("execution-time")).isNotNull();
}
@Test
- public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() {
+ void callShouldReturnResultWithUpdateActionWhenOptableTargetingReturnsTargeting() {
+ // given
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() {
+ // given
+ final ModuleContext moduleContext = new ModuleContext();
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, false)), 0.01);
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ moduleContext.setOptableTargetingCall(
+ networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties(
+ "key", "tenant", "origin", false)));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() {
+ // given
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, false)), 0.01);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids())
+ .flatExtracting(Eid::getUids)
+ .extracting(Uid::getId)
+ .containsExactly("id");
+ assertThat(bidRequest.getUser().getData())
+ .flatExtracting(Data::getSegment)
+ .extracting(Segment::getId)
+ .containsExactly("id");
+ }
+
+ @Test
+ void callShouldReturnResultWithoutEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() {
// given
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, true)), 0.01);
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any()))
.thenReturn(Future.succeededFuture(givenTargetingResult()));
@@ -119,30 +247,62 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ final BidRequest bidRequest = result
+ .payloadUpdate()
+ .apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
+ .bidRequest();
+ assertThat(bidRequest.getUser().getEids()).isNull();
+ assertThat(bidRequest.getUser().getData()).isNull();
+ }
+
+ @Test
+ void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() {
+ // given
+ final ModuleContext moduleContext = new ModuleContext();
+ target = new OptableTargetingProcessedAuctionRequestHook(
+ configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, true)), 0.01);
+ when(optableTargeting.getTargeting(any(), any(), any(), any()))
+ .thenReturn(Future.succeededFuture(givenTargetingResult()));
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+ when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
+ moduleContext.setOptableTargetingCall(
+ networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties(
+ "key", "tenant", "origin", false)));
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
final BidRequest bidRequest = result
.payloadUpdate()
.apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
.bidRequest();
- assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id");
- assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id");
+ assertThat(bidRequest.getUser().getEids()).isNull();
+ assertThat(bidRequest.getUser().getData()).isNull();
}
@Test
- public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() {
+ void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() {
// given
configResolver = new ConfigResolver(
mapper,
jsonMerger,
givenOptableTargetingProperties("key", "tenant", null, false));
target = new OptableTargetingProcessedAuctionRequestHook(
- configResolver,
- optableTargeting,
- userFpdActivityMask,
- 0.01);
+ configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
when(invocationContext.accountConfig())
.thenReturn(givenAccountConfig("key", "tenant", null, true));
@@ -155,26 +315,23 @@ public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
assertThat((ModuleContext) result.moduleContext())
.extracting(it -> it.getEnrichRequestStatus().getStatus())
.isEqualTo(Status.FAIL);
}
@Test
- public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() {
+ void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() {
// given
configResolver = new ConfigResolver(
mapper,
jsonMerger,
givenOptableTargetingProperties("key", null, "origin", false));
target = new OptableTargetingProcessedAuctionRequestHook(
- configResolver,
- optableTargeting,
- userFpdActivityMask,
- 0.01);
+ configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01);
when(invocationContext.accountConfig())
.thenReturn(givenAccountConfig("key", null, null, true));
@@ -187,18 +344,17 @@ public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action);
assertThat((ModuleContext) result.moduleContext())
.extracting(it -> it.getEnrichRequestStatus().getStatus())
.isEqualTo(Status.FAIL);
}
@Test
- public void shouldReturnResultWithCleanedUpUserExtOptableTag() {
+ void callShouldReturnResultWithCleanedUpUserExtOptableTag() {
// given
- when(invocationContext.timeout()).thenReturn(timeout);
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any()))
.thenReturn(Future.succeededFuture(givenTargetingResult()));
@@ -212,10 +368,10 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() {
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
final ObjectNode optable = (ObjectNode) result
.payloadUpdate()
.apply(AuctionRequestPayloadImpl.of(givenBidRequest()))
@@ -226,7 +382,7 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() {
}
@Test
- public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() {
+ void callShouldReturnResultWithUpdateWhenOptableTargetingDoesNotReturnResult() {
// given
when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest());
when(optableTargeting.getTargeting(any(), any(), any(), any())).thenReturn(Future.succeededFuture(null));
@@ -240,17 +396,54 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult()
assertThat(future.succeeded()).isTrue();
final InvocationResult result = future.result();
- assertThat(result).isNotNull();
- assertThat(result.status()).isEqualTo(InvocationStatus.success);
- assertThat(result.action()).isEqualTo(InvocationAction.update);
- assertThat(result.errors()).isNull();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
+ }
+
+ @Test
+ void callShouldReturnUpdateWhenTrafficSourceIsInvalid() {
+ // given
+ final ModuleContext moduleContext = new ModuleContext();
+ moduleContext.setShouldSkipEnrichment(true);
+ when(invocationContext.moduleContext()).thenReturn(moduleContext);
+
+ // when
+ final Future> future = target.call(auctionRequestPayload,
+ invocationContext);
+
+ // then
+ assertThat(future).isNotNull();
+ assertThat(future.succeeded()).isTrue();
+
+ final InvocationResult result = future.result();
+ assertThat(result).isNotNull()
+ .returns(InvocationStatus.success, InvocationResult::status)
+ .returns(InvocationAction.update, InvocationResult::action)
+ .extracting(InvocationResult::errors).isNull();
}
private ObjectNode givenAccountConfig(boolean cacheEnabled) {
return givenAccountConfig("key", "tenant", "origin", cacheEnabled);
}
- private ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) {
- return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled));
+ private ExecutionPlan givenExecutionPlan(boolean hasRawAuctionRequestHook, boolean hasBidderRequestHook) {
+ final HookId rawAuctionHook = HookId.of("optable-targeting", "optable-targeting-raw-auction-request-hook");
+ final HookId bidderRequestHook = HookId.of("optable-targeting", "optable-targeting-bidder-request-hook");
+
+ final StageExecutionPlan rawAuctionStage = StageExecutionPlan.of(List.of(
+ ExecutionGroup.of(null, hasRawAuctionRequestHook ? List.of(rawAuctionHook) : List.of())
+ ));
+ final StageExecutionPlan bidderRequestStage = StageExecutionPlan.of(List.of(
+ ExecutionGroup.of(null, hasBidderRequestHook ? List.of(bidderRequestHook) : List.of())
+ ));
+
+ final EndpointExecutionPlan endpointExecutionPlan = EndpointExecutionPlan.of(Map.of(
+ Stage.raw_auction_request, rawAuctionStage,
+ Stage.bidder_request, bidderRequestStage
+ ));
+
+ return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointExecutionPlan));
}
}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java
new file mode 100644
index 00000000000..b8b7c986636
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java
@@ -0,0 +1,88 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.BidRequest;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.bidder.BidderCatalog;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequest;
+import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid;
+
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+public class AliasesResolverTest {
+
+ @Mock
+ private BidderCatalog bidderCatalog;
+
+ private AliasesResolver target;
+
+ @BeforeEach
+ public void setUp() {
+ target = AliasesResolver.of(bidderCatalog);
+ }
+
+ @Test
+ public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestIsNull() {
+ // when
+ final BidderAliases result = target.resolve(null);
+
+ // then
+ assertThat(result.isAliasDefined("anyAlias")).isFalse();
+ }
+
+ @Test
+ public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestHasNoExt() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder().build();
+
+ // when
+ final BidderAliases result = target.resolve(bidRequest);
+
+ // then
+ assertThat(result.isAliasDefined("anyAlias")).isFalse();
+ }
+
+ @Test
+ public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestHasNoExtPrebid() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .ext(ExtRequest.empty())
+ .build();
+
+ // when
+ final BidderAliases result = target.resolve(bidRequest);
+
+ // then
+ assertThat(result.isAliasDefined("anyAlias")).isFalse();
+ }
+
+ @Test
+ public void resolveShouldReturnBidderAliasesWithValuesWhenBidRequestHasAliases() {
+ // given
+ final BidRequest bidRequest = BidRequest.builder()
+ .ext(ExtRequest.of(ExtRequestPrebid.builder()
+ .aliases(Map.of("alias", "bidder"))
+ .aliasgvlids(Map.of("alias", 123))
+ .build()))
+ .build();
+
+ given(bidderCatalog.isValidName(anyString())).willReturn(false);
+
+ // when
+ final BidderAliases result = target.resolve(bidRequest);
+
+ // then
+ assertThat(result.isAliasDefined("alias")).isTrue();
+ assertThat(result.resolveBidder("alias")).isEqualTo("bidder");
+ assertThat(result.resolveAliasVendorId("alias")).isEqualTo(123);
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java
new file mode 100644
index 00000000000..bfcc3057b76
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java
@@ -0,0 +1,284 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Imp;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.prebid.server.auction.aliases.BidderAliases;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.IntSupplier;
+import java.util.function.UnaryOperator;
+
+import static java.util.function.UnaryOperator.identity;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.BDDMockito.given;
+
+@ExtendWith(MockitoExtension.class)
+public class BidderEnrichmentSamplerTest extends BaseOptableTest {
+
+ @Mock
+ private AliasesResolver aliasesResolver;
+
+ @Mock
+ private BidderAliases bidderAliases;
+
+ @Mock
+ private IntSupplier randomSupplier;
+
+ private BidderEnrichmentSampler target;
+
+ @BeforeEach
+ public void setUp() {
+ target = BidderEnrichmentSampler.of(aliasesResolver, randomSupplier);
+ given(aliasesResolver.resolve(any())).willReturn(bidderAliases);
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenRequestHasNoImpressions() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(identity());
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenImpHasNoExt() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(identity()))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenImpExtHasNoPrebidBidderNode() {
+ // given
+ final ObjectNode ext = mapper.createObjectNode();
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(ext)))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldReturnEmptySetWhenBidderNodeIsEmpty() {
+ // given
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt())))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldIncludeAllBiddersWhenDefaultPercentageIs100() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(99);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactlyInAnyOrder("bidderA", "bidderB");
+ }
+
+ @Test
+ public void sampleShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(0);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(-1, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldIncludeBidderWhenRandomValueEqualsPercentage() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(50);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ @Test
+ public void sampleShouldIncludeBidderWhenRandomValueIsBelowPercentage() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(49);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ @Test
+ public void sampleShouldExcludeBidderWhenRandomValueExceedsPercentage() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(51);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldIncludeBidderWhenPercentageIsZeroAndRandomIsZero() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(0);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(0, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ @Test
+ public void sampleShouldExcludeBidderWhenPercentageIsZeroAndRandomIsOne() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(1);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(0, Collections.emptyMap()));
+
+ // then
+ assertThat(result).isEmpty();
+ }
+
+ @Test
+ public void sampleShouldUseBidderSpecificPercentageWhenAvailable() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(99);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(-1, Map.of("bidderA", 100)));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ @Test
+ public void sampleShouldUseAliasSpecificPercentageWhenBidderResolvesToAlias() {
+ // given
+ given(bidderAliases.resolveBidder("bidderA")).willReturn("bidderA");
+ given(bidderAliases.resolveBidder("bidderB")).willReturn("aliasB");
+ given(randomSupplier.getAsInt()).willReturn(99);
+
+ final BidRequest bidRequest = givenBidRequest(
+ request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB"))))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(-1, Map.of("aliasB", 100)));
+
+ // then
+ assertThat(result).containsExactly("bidderB");
+ }
+
+ @Test
+ public void sampleShouldDeduplicateBiddersAppearingInMultipleImps() {
+ // given
+ given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0));
+ given(randomSupplier.getAsInt()).willReturn(0);
+
+ final ObjectNode ext = givenBidderExt("bidderA");
+ final BidRequest bidRequest = givenBidRequest(request -> request.imp(List.of(
+ givenImp(imp -> imp.ext(ext)),
+ givenImp(imp -> imp.ext(ext)))));
+
+ // when
+ final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap()));
+
+ // then
+ assertThat(result).containsExactly("bidderA");
+ }
+
+ private OptableTargetingProperties givenSampleProperties(int defaultPct, Map bidderPcts) {
+ final OptableTargetingProperties props = new OptableTargetingProperties();
+ props.setEnrichmentPercentage(defaultPct);
+ props.setBidderEnrichmentPercentages(bidderPcts);
+ return props;
+ }
+
+ private ObjectNode givenBidderExt(String... bidders) {
+ final ObjectNode bidderNode = mapper.createObjectNode();
+ for (String bidder : bidders) {
+ bidderNode.put(bidder, "value");
+ }
+ final ObjectNode prebidNode = mapper.createObjectNode();
+ prebidNode.set("bidder", bidderNode);
+ final ObjectNode ext = mapper.createObjectNode();
+ ext.set("prebid", prebidNode);
+ return ext;
+ }
+
+ private static Imp givenImp(UnaryOperator impCustomizer) {
+ return impCustomizer.apply(Imp.builder()).build();
+ }
+}
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java
new file mode 100644
index 00000000000..08d4faa9116
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java
@@ -0,0 +1,252 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.execution.model.EndpointExecutionPlan;
+import org.prebid.server.hooks.execution.model.ExecutionGroup;
+import org.prebid.server.hooks.execution.model.ExecutionPlan;
+import org.prebid.server.hooks.execution.model.HookId;
+import org.prebid.server.hooks.execution.model.Stage;
+import org.prebid.server.hooks.execution.model.StageExecutionPlan;
+import org.prebid.server.model.Endpoint;
+import org.prebid.server.settings.model.Account;
+import org.prebid.server.settings.model.AccountHooksConfiguration;
+
+import java.util.List;
+import java.util.Map;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class CompositeHookExecutionPlanTest {
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnTrueWhenGlobalPlanHasHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnTrueWhenAccountPlanHasHook() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnTrueWhenBothPlansHaveHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenNeitherPlanHasHook() {
+ // given
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenAccountIsNull() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(null)).isFalse();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenAccountIdIsEmpty() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnGlobalFlagWhenAccountHasNoHooksConfig() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ assertThat(target.hasRawAuctionRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnTrueWhenGlobalPlanHasHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnTrueWhenAccountPlanHasHook() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnTrueWhenBothPlansHaveHook() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenNeitherPlanHasHook() {
+ // given
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenAccountIsNull() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(null)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenAccountIdIsEmpty() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnGlobalFlagWhenAccountHasNoHooksConfig() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() {
+ // given
+ final ExecutionPlan accountPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null);
+ final Account account = givenAccount("accountId", accountPlan);
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ assertThat(target.hasBidderRequestHook(account)).isTrue();
+ }
+
+ @Test
+ public void hasRawAuctionRequestHookShouldReturnFalseWhenOnlyBidderRequestHookIsInGlobalPlan() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "bidder_request", "optable-targeting-bidder-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasRawAuctionRequestHook(account)).isFalse();
+ }
+
+ @Test
+ public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIsInGlobalPlan() {
+ // given
+ final ExecutionPlan globalPlan = givenExecutionPlan(
+ "raw_auction_request", "optable-targeting-raw-auction-request-hook");
+ final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan);
+ final Account account = Account.builder().id("accountId").build();
+
+ // when and then
+ assertThat(target.hasBidderRequestHook(account)).isFalse();
+ }
+
+ private ExecutionPlan givenExecutionPlan(String stage, String hookCode) {
+ final HookId hookId = HookId.of("optable-targeting", hookCode);
+ final ExecutionGroup group = ExecutionGroup.of(null, List.of(hookId));
+ final StageExecutionPlan stagePlan = StageExecutionPlan.of(List.of(group));
+ final EndpointExecutionPlan endpointPlan = EndpointExecutionPlan.of(Map.of(Stage.valueOf(stage), stagePlan));
+ return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointPlan));
+ }
+
+ private Account givenAccount(String accountId, ExecutionPlan executionPlan) {
+ return Account.builder()
+ .id(accountId)
+ .hooks(AccountHooksConfiguration.of(executionPlan, null, null))
+ .build();
+ }
+}
+
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java
index de2c01948fb..9621758cab0 100644
--- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java
@@ -2,6 +2,8 @@
import com.iab.gpp.encoder.GppModel;
import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Regs;
+import com.iab.openrtb.request.User;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -15,6 +17,8 @@
import org.prebid.server.privacy.gdpr.model.TcfContext;
import org.prebid.server.privacy.model.Privacy;
import org.prebid.server.privacy.model.PrivacyContext;
+import org.prebid.server.proto.openrtb.ext.request.ExtRegs;
+import org.prebid.server.proto.openrtb.ext.request.ExtUser;
import java.util.List;
import java.util.Set;
@@ -42,15 +46,32 @@ public void setUp() {
}
@Test
- public void shouldResolveTcfAttributesWhenConsentIsValid() {
+ public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() {
// given
final GppModel gppModel = mock();
- when(tcfContext.isConsentValid()).thenReturn(true);
- when(tcfContext.isInGdprScope()).thenReturn(true);
- when(tcfContext.getConsentString()).thenReturn("consent");
when(gppModel.encode()).thenReturn("consent");
when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1)));
- final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext);
+ final AuctionContext auctionContext =
+ givenAuctionContext(givenBidRequestWithGdprORTB26(true, "consent"), tcfContext, gppContext);
+
+ // when
+ final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
+ auctionContext, properties.getTimeout());
+
+ // then
+ assertThat(result).isNotNull()
+ .returns(true, OptableAttributes::isGdprApplies)
+ .returns("consent", OptableAttributes::getGdprConsent);
+ }
+
+ @Test
+ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() {
+ // given
+ final GppModel gppModel = mock();
+ when(gppModel.encode()).thenReturn("consent");
+ when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1)));
+ final AuctionContext auctionContext =
+ givenAuctionContext(givenBidRequestWithGdprORTB25(true, "consent"), tcfContext, gppContext);
// when
final OptableAttributes result = OptableAttributesResolver.resolveAttributes(
@@ -62,6 +83,34 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() {
.returns("consent", OptableAttributes::getGdprConsent);
}
+ private BidRequest givenBidRequestWithGdprORTB26(boolean isGdprEnabled, String consent) {
+ final User user = User.builder()
+ .consent(consent)
+ .build();
+
+ return BidRequest.builder()
+ .user(user)
+ .regs(Regs.builder()
+ .gdpr(isGdprEnabled ? 1 : 0)
+ .build())
+ .build();
+ }
+
+ private BidRequest givenBidRequestWithGdprORTB25(boolean isGdprEnabled, String consent) {
+ final User user = User.builder()
+ .ext(ExtUser.builder()
+ .consent(consent)
+ .build())
+ .build();
+
+ return BidRequest.builder()
+ .user(user)
+ .regs(Regs.builder()
+ .ext(ExtRegs.of(isGdprEnabled ? 1 : 0, null, null, null))
+ .build())
+ .build();
+ }
+
@Test
public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() {
// given
diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java
new file mode 100644
index 00000000000..0f496051b65
--- /dev/null
+++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java
@@ -0,0 +1,126 @@
+package org.prebid.server.hooks.modules.optable.targeting.v1.core;
+
+import com.iab.openrtb.request.App;
+import com.iab.openrtb.request.BidRequest;
+import com.iab.openrtb.request.Site;
+import org.junit.jupiter.api.Test;
+import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class PropertiesValidatorTest {
+
+ @Test
+ public void isValidShouldReturnTrueWhenTenantAndOriginArePresent() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setTenant("tenant");
+ properties.setOrigin("origin");
+
+ // when
+ final boolean result = PropertiesValidator.isValid(properties);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isValidShouldReturnFalseWhenTenantIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setOrigin("origin");
+
+ // when
+ final boolean result = PropertiesValidator.isValid(properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isValidShouldReturnFalseWhenOriginIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setTenant("tenant");
+
+ // when
+ final boolean result = PropertiesValidator.isValid(properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnTrueWhenEnrichWebIsTrueAndSiteIsPresent() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichWeb(true);
+ final BidRequest bidRequest = BidRequest.builder().site(Site.builder().build()).build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnFalseWhenEnrichWebIsTrueAndSiteIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichWeb(true);
+ final BidRequest bidRequest = BidRequest.builder().build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnTrueWhenEnrichAppIsTrueAndAppIsPresent() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichApp(true);
+ final BidRequest bidRequest = BidRequest.builder().app(App.builder().build()).build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isTrue();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnFalseWhenEnrichAppIsTrueAndAppIsMissing() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichApp(true);
+ final BidRequest bidRequest = BidRequest.builder().build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+
+ @Test
+ public void isTrafficSourceValidShouldReturnFalseWhenBothEnrichWebAndEnrichAppAreFalseOrNull() {
+ // given
+ final OptableTargetingProperties properties = new OptableTargetingProperties();
+ properties.setEnrichWeb(false);
+ properties.setEnrichApp(false);
+ final BidRequest bidRequest = BidRequest.builder()
+ .site(Site.builder().build())
+ .app(App.builder().build())
+ .build();
+
+ // when
+ final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties);
+
+ // then
+ assertThat(result).isFalse();
+ }
+}
diff --git a/sample/configs/prebid-config-with-optable_v2.yaml b/sample/configs/prebid-config-with-optable_v2.yaml
new file mode 100644
index 00000000000..1d30adf7992
--- /dev/null
+++ b/sample/configs/prebid-config-with-optable_v2.yaml
@@ -0,0 +1,53 @@
+status-response: "ok"
+adapters:
+ appnexus:
+ enabled: true
+ ix:
+ enabled: true
+ openx:
+ enabled: true
+ pubmatic:
+ enabled: true
+ rubicon:
+ enabled: true
+ improvedigital:
+ enabled: true
+ colossus:
+ enabled: true
+ triplelift:
+ enabled: true
+metrics:
+ prefix: prebid
+cache:
+ scheme: http
+ host: localhost
+ path: /cache
+ query: uuid=
+settings:
+ enforce-valid-account: false
+ generate-storedrequest-bidrequest-id: true
+ filesystem:
+ settings-filename: sample/configs/sample-app-settings-optable_v2.yaml
+ stored-requests-dir: sample
+ stored-imps-dir: sample
+ stored-responses-dir: sample/stored
+ categories-dir:
+gdpr:
+ default-value: 1
+ vendorlist:
+ v2:
+ cache-dir: /var/tmp/vendor2
+ v3:
+ cache-dir: /var/tmp/vendor3
+admin-endpoints:
+ logging-changelevel:
+ enabled: true
+ path: /logging/changelevel
+ on-application-port: true
+ protected: false
+hooks:
+ optable-targeting:
+ enabled: true
+ modules:
+ optable-targeting:
+ api-endpoint: https://na.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}}
diff --git a/sample/configs/sample-app-settings-optable_v2.yaml b/sample/configs/sample-app-settings-optable_v2.yaml
new file mode 100644
index 00000000000..5bb0d589483
--- /dev/null
+++ b/sample/configs/sample-app-settings-optable_v2.yaml
@@ -0,0 +1,97 @@
+accounts:
+ - id: 1
+ status: active
+ auction:
+ price-granularity: low
+ privacy:
+ ccpa:
+ enabled: true
+ gdpr:
+ enabled: true
+ cookie-sync:
+ default-limit: 8
+ max-limit: 15
+ coop-sync:
+ default: true
+ analytics:
+ allow-client-details: true
+ hooks:
+ modules:
+ optable-targeting:
+ api-key: key
+ tenant: optable
+ origin: web-sdk-demo
+ enrichment-percentage: 100
+ bidder-enrichment-percentages:
+ appnexus: 75
+ rubicon: 75
+ pubmatic: 100
+ criteo: 0
+ enrich-web: true
+ enrich-app: true
+ ppid-mapping: { "pubcid.org": "c" }
+ adserver-targeting: true
+ cache:
+ enabled: false
+ ttlseconds: 86400
+ execution-plan:
+ {
+ "endpoints": {
+ "/openrtb2/auction": {
+ "stages": {
+ "raw-auction-request": {
+ "groups": [
+ {
+ "timeout": 1000,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-raw-auction-request-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "bidder-request": {
+ "groups": [
+ {
+ "timeout": 500,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-bidder-request-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "processed-auction-request": {
+ "groups": [
+ {
+ "timeout": 600,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-processed-auction-request-hook"
+ }
+ ]
+ }
+ ]
+ },
+ "auction-response": {
+ "groups": [
+ {
+ "timeout": 10,
+ "hook-sequence": [
+ {
+ "module-code": "optable-targeting",
+ "hook-impl-code": "optable-targeting-auction-response-hook"
+ }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ }
+ }