diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 431f437d1f0..307b3f04737 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -156,19 +156,21 @@ Sample module enablement configuration in JSON and YAML formats: Previous versions used only the `processed-auction-request` hook (with `auction-response`), which made the API call and enriched the whole request synchronously, blocking the auction. To migrate, keep that hook and add the -`raw-auction-request` and `bidder-request` hooks as shown above. With them in the plan, the processed hook no longer -blocks; without them, it keeps the legacy synchronous behavior. +`raw-auction-request` and `bidder-request` hooks as shown above. With the `bidder-request` hook in the plan, the +processed hook no longer blocks; without it, it keeps the legacy synchronous behavior. In that mode the whole request is +enriched, so it is enriched for all bidders whenever sampling selects any of them. ### Timeout considerations -The `bidder-request` hook timeout is the budget for the API call started in the `raw-auction-request` or -`processed-auction-request` stage. The call runs in parallel with the rest of the auction, so the actual wait at the -`bidder-request` stage is usually much shorter than the full roundtrip. The `raw-auction-request` and +The API call is started in the `raw-auction-request` or `processed-auction-request` stage and runs in parallel with the +rest of the auction, so the actual wait at the `bidder-request` stage is usually much shorter than the full roundtrip. +The `api-timeout` module parameter limits the call itself (by default, the time remaining for the auction), and the +`bidder-request` hook timeout limits how long a bidder request waits for it. The `raw-auction-request` and `processed-auction-request` hook timeouts only cover validation and sampling and can be kept short. -**Note:** Do not confuse hook timeout value with the module timeout parameter which is optional. The hook timeout value -would depend on the cloud/region where the PBS instance is hosted and the latency to reach the Optable's servers. This -will need to be verified experimentally upon deployment. +**Note:** Do not confuse these with the module `timeout` parameter, an optional hint passed to the Targeting API. The +`api-timeout` and hook timeout values depend on the cloud/region where the PBS instance is hosted and the latency to +reach the Optable's servers, and need to be verified experimentally upon deployment. The timeout value for the `auction-response` can be set to 10 ms - usually it will be sub-millisecond time as there are no HTTP calls made in this hook - Optable-specific keywords are cached on earlier stages and retrieved from the module @@ -196,6 +198,7 @@ would result in this nesting in the JSON configuration: | ppid-mapping | no | map | none | This specifies PPID source (`user.ext.eids[].source`) to a custom identifier prefix mapping, f.e. `{"example.com" : "c"}`. See the section on ID Mapping below for more detail. | | adserver-targeting | no | boolean | false | If set to true - will add the Optable-specific adserver targeting keywords into the PBS response for every `seatbid[].bid[].ext.prebid.targeting` | | timeout | no | integer | none | A soft timeout (in ms) sent as a hint to the Targeting API endpoint to limit the request times to Optable's external tokenizer services | +| api-timeout | no | integer | none | A hard timeout (in ms) for the Targeting API call awaited by the `bidder-request` hook. When not set, the call is limited by the time remaining for the auction. See Timeout considerations above. | | id-prefix-order | no | string | none | An optional string of comma separated id prefixes that prioritizes and specifies the order in which ids are provided to Targeting API in a query string. F.e. "c,c1,id5" will guarantee that Targeting API will see id=c:...,c1:...,id5:... if these ids are provided. id-prefixes not mentioned in this list will be added in arbitrary order after the priority prefix ids. This affects Targeting API processing logic | | hid-prefixes | no | string | none | An optional string of comma separated id prefixes that should additionally be sent to the Targeting API as resolver hints in `hid=prefix:value` query parameters. See the section on Resolver Hints (hid) below for more detail. | | enrichment-percentage | no | integer | 100 | Default percentage (0-100) of bid requests per bidder that will receive enrichment data. Set to 100 to enrich all requests, 0 to disable enrichment by default. | 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 ebdc4f0ee0f..6b11c219247 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 @@ -119,7 +119,8 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, - earlyOptableCallResolver), + earlyOptableCallResolver, + logSamplingRate), new OptableBidderRequestHook(), new OptableTargetingAuctionResponseHook( configResolver, @@ -136,17 +137,22 @@ BidderEnrichmentSampler bidderEnrichmentSampler(BidderCatalog bidderCatalog) { OptableTargetingFlowResolver earlyOptableCallResolver( BidderEnrichmentSampler bidderEnrichmentSampler, TargetingRequestExecutor targetingRequestExecutor, - @Value("${hooks.host-execution-plan:}") - String executionPlan, + @Value("${hooks.host-execution-plan:}") String hostExecutionPlan, + @Value("${hooks.default-account-execution-plan:}") String defaultAccountExecutionPlan, JacksonMapper mapper, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { final CompositeHookExecutionPlan hooksExecutionPlan = CompositeHookExecutionPlan.of( - StringUtils.isNoneEmpty(executionPlan) - ? mapper.decodeValue(executionPlan, ExecutionPlan.class) - : null); + parseExecutionPlan(hostExecutionPlan, mapper), + parseExecutionPlan(defaultAccountExecutionPlan, mapper)); return new OptableTargetingFlowResolver( bidderEnrichmentSampler, targetingRequestExecutor, hooksExecutionPlan, logSamplingRate); } + + private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { + return StringUtils.isNotBlank(executionPlan) + ? mapper.decodeValue(executionPlan, ExecutionPlan.class) + : ExecutionPlan.empty(); + } } 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 64e18dfedaf..7ced31bbe66 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,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.model; +import com.fasterxml.jackson.databind.JsonNode; import io.vertx.core.Future; import lombok.Data; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; @@ -37,6 +38,8 @@ public class ModuleContext { private boolean isEarlyCallInitializationCompleted = true; + private JsonNode extUserOptable; + private String id5Signature; public static ModuleContext of(AuctionInvocationContext invocationContext) { 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 314e2639cef..527eb14f53d 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 @@ -29,6 +29,9 @@ public final class OptableTargetingProperties { Long timeout; + @JsonProperty("api-timeout") + Long apiTimeout; + @JsonProperty("id-prefix-order") String idPrefixOrder; 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 index 8d867246c9e..838b4a714aa 100644 --- 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 @@ -44,10 +44,31 @@ public OptableRawAuctionRequestHook(ConfigResolver configResolver, 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()); + + // whatever goes wrong here, the cleaner has to be applied, or user.ext.optable ids reach the bidders + try { + return resolveTargetingFlow(payload, invocationContext, moduleContext); + } catch (RuntimeException e) { + conditionalLogger.error("Failed to initiate Optable targeting call: " + e.getMessage(), logSamplingRate); + + moduleContext.setEarlyCallInitializationCompleted(true); + moduleContext.setExtUserOptable(null); + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + + return update(AuctionRequestCleaner.instance(), moduleContext); + } + } + + private Future> resolveTargetingFlow( + AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + ModuleContext moduleContext) { + + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); moduleContext.setOptableTargetingProperties(properties); if (!PropertiesValidator.isValid(properties)) { @@ -61,7 +82,7 @@ public Future> call(AuctionRequestPayloa } return optableTargetingFlowResolver.resolveAsyncOptableTargetingFlow( - moduleContext, payload, invocationContext, properties, false); + moduleContext, payload, invocationContext, properties); } public static Future> update( @@ -77,16 +98,6 @@ public static Future> update( .build()); } - public static Future> success(ModuleContext moduleContext) { - - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .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/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 34449d9b049..a502e3c6608 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,17 +1,16 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import io.vertx.core.Future; -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.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargetingFlowResolver; -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.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.util.Objects; @@ -19,15 +18,22 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc public static final String CODE = "optable-targeting-processed-auction-request-hook"; + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); + private final ConfigResolver configResolver; private final OptableTargetingFlowResolver optableTargetingFlowResolver; + private final double logSamplingRate; + public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, - OptableTargetingFlowResolver earlyOptableCallResolver) { + OptableTargetingFlowResolver earlyOptableCallResolver, + double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); this.optableTargetingFlowResolver = Objects.requireNonNull(earlyOptableCallResolver); + this.logSamplingRate = logSamplingRate; } @Override @@ -35,28 +41,19 @@ public Future> call(AuctionRequestPayloa AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = ModuleContext.of(invocationContext); - final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); - if (moduleContext.isEarlyNetworkCallEnabled()) { - if (moduleContext.isEarlyCallInitializationCompleted()) { - return success(moduleContext); - } else { - return optableTargetingFlowResolver.resolveAsyncOptableTargetingFlow( - moduleContext, auctionRequestPayload, invocationContext, properties, true); - } + // whatever goes wrong here, the cleaner has to be applied, or user.ext.optable ids reach the bidders + try { + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + return optableTargetingFlowResolver.resolveOptableTargetingFlow( + auctionRequestPayload, invocationContext, moduleContext, properties); + } catch (RuntimeException e) { + conditionalLogger.error("Failed to initiate Optable targeting call: " + e.getMessage(), logSamplingRate); + + moduleContext.setEarlyCallInitializationCompleted(true); + moduleContext.setExtUserOptable(null); + return optableTargetingFlowResolver.failed(moduleContext); } - - return optableTargetingFlowResolver.resolveOptableTargetingFlow( - auctionRequestPayload, invocationContext, moduleContext, properties); - } - - public static Future> success(ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .moduleContext(moduleContext) - .build()); } @Override 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 index 504c590f859..2512f8fd354 100644 --- 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 @@ -53,6 +53,10 @@ public Set sample(BidRequest bidRequest, OptableTargetingProperties opta .collect(Collectors.toSet()); } + public boolean hasBidders(BidRequest bidRequest) { + return !extractUniqueBidders(bidRequest).isEmpty(); + } + private static int resolvePercentage(BidderAliases aliases, String bidder, int defaultEnrichmentPercentage, Map bidderEnrichmentPercentage) { @@ -86,8 +90,4 @@ private static Stream extractImpBidders(ObjectNode impExt) { .filter(Ortb2ImplicitParametersResolver::isImpExtBidder) .filter(field -> impExt.get(field).isObject()); } - - public boolean hasBidders(BidRequest bidRequest) { - return !extractUniqueBidders(bidRequest).isEmpty(); - } } 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 index 0d5bda38be9..1cb703a415f 100644 --- 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 @@ -1,120 +1,72 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.model.AuctionContext; 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.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookHttpEndpoint; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook; -import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountHooksConfiguration; -import java.util.List; +import java.util.Collection; +import java.util.Objects; import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; +/** + * Resolves the hooks configured for a request the way the core does: the host execution plan combined with the + * account execution plan, or with the default account execution plan when the account has none, for the endpoint + * of the request. + */ public class CompositeHookExecutionPlan { - private static final HookHttpEndpoint ENDPOINT_AUCTION = HookHttpEndpoint.POST_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 = OptableRawAuctionRequestHook.CODE; - private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = OptableBidderRequestHook.CODE; + private final ExecutionPlan hostExecutionPlan; + private final ExecutionPlan defaultAccountExecutionPlan; - private final boolean hasGlobalRawAuctionRequestHook; - - private final boolean hasGlobalBidderRequestHook; - - private final long globalBidderRequestHookTimeout; - - private final ConcurrentHashMap rawAuctionRequestHookCache = new ConcurrentHashMap<>(); - private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>(); - - private final ConcurrentHashMap bidderRequestHookTimeoutCache = new ConcurrentHashMap<>(); - - private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook, - boolean hasGlobalBidderRequestHook, - long globalBidderRequestHookTimeout) { - - this.hasGlobalRawAuctionRequestHook = hasGlobalRawAuctionRequestHook; - this.hasGlobalBidderRequestHook = hasGlobalBidderRequestHook; - this.globalBidderRequestHookTimeout = globalBidderRequestHookTimeout; - } - - public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) { - return globalExecutionPlan == null - ? new CompositeHookExecutionPlan(false, false, 0) - : new CompositeHookExecutionPlan( - hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION), - hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST), - getHookTimeout(globalExecutionPlan, - STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)); + private CompositeHookExecutionPlan(ExecutionPlan hostExecutionPlan, ExecutionPlan defaultAccountExecutionPlan) { + this.hostExecutionPlan = Objects.requireNonNull(hostExecutionPlan); + this.defaultAccountExecutionPlan = Objects.requireNonNull(defaultAccountExecutionPlan); } - private T computeFromAccount(Account account, - ConcurrentHashMap cache, - T defaultValue, - Function compute) { - final String accountId = account != null ? account.getId() : null; - return StringUtils.isNotEmpty(accountId) - ? cache.computeIfAbsent(accountId, id -> compute.apply(resolveExecutionPlan(account))) - : defaultValue; - } + public static CompositeHookExecutionPlan of(ExecutionPlan hostExecutionPlan, + ExecutionPlan defaultAccountExecutionPlan) { - public boolean hasRawAuctionRequestHook(Account account) { - return computeFromAccount(account, rawAuctionRequestHookCache, false, - plan -> hasHook(plan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) - || hasGlobalRawAuctionRequestHook); + return new CompositeHookExecutionPlan( + Objects.requireNonNullElse(hostExecutionPlan, ExecutionPlan.empty()), + Objects.requireNonNullElse(defaultAccountExecutionPlan, ExecutionPlan.empty())); } - public boolean hasBidderRequestHook(Account account) { - return computeFromAccount(account, bidderRequestHookCache, false, - plan -> hasHook(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST) - || hasGlobalBidderRequestHook); - } + public boolean hasBidderRequestHook(AuctionContext auctionContext) { + final HookHttpEndpoint endpoint = Optional.ofNullable(auctionContext) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getEndpoint) + .orElse(HookHttpEndpoint.POST_AUCTION); - public long getOptableTargetingBidderRequestTimeout(Account account) { - return computeFromAccount(account, bidderRequestHookTimeoutCache, globalBidderRequestHookTimeout, - plan -> { - final long timeout = getHookTimeout(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST); - return timeout != 0 ? timeout : globalBidderRequestHookTimeout; - }); + return hasHook(hostExecutionPlan, endpoint) + || hasHook(accountExecutionPlan(auctionContext != null ? auctionContext.getAccount() : null), endpoint); } - private ExecutionPlan resolveExecutionPlan(Account account) { + private ExecutionPlan accountExecutionPlan(Account account) { return Optional.ofNullable(account) - .map(org.prebid.server.settings.model.Account::getHooks) - .map(org.prebid.server.settings.model.AccountHooksConfiguration::getExecutionPlan) - .orElse(null); + .map(Account::getHooks) + .map(AccountHooksConfiguration::getExecutionPlan) + .orElse(defaultAccountExecutionPlan); } - private static boolean hasHook(ExecutionPlan executionPlan, String stage, String hookCode) { - return Optional.ofNullable(executionPlan) - .map(ExecutionPlan::getEndpoints) - .map(endpoints -> endpoints.get(ENDPOINT_AUCTION)) + private static boolean hasHook(ExecutionPlan executionPlan, HookHttpEndpoint endpoint) { + return Optional.ofNullable(executionPlan.getEndpoints()) + .map(endpoints -> endpoints.get(endpoint)) .map(EndpointExecutionPlan::getStages) - .map(stages -> stages.get(Stage.valueOf(stage))) + .map(stages -> stages.get(Stage.bidder_request)) .map(StageExecutionPlan::getGroups) - .orElseGet(List::of) .stream() + .flatMap(Collection::stream) .map(ExecutionGroup::getHookSequence) - .flatMap(java.util.Collection::stream) - .anyMatch(hook -> hookCode.equals(hook.getHookImplCode())); - } - - private static long getHookTimeout(ExecutionPlan executionPlan, String stage, String hookCode) { - return Optional.ofNullable(executionPlan) - .map(ExecutionPlan::getEndpoints) - .map(endpoints -> endpoints.get(ENDPOINT_AUCTION)) - .map(EndpointExecutionPlan::getStages) - .map(stages -> stages.get(Stage.valueOf(stage))) - .map(StageExecutionPlan::getGroups) - .orElseGet(List::of) - .stream().findFirst() - .map(ExecutionGroup::getTimeout) - .orElse(0L); + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .anyMatch(hookId -> OptableBidderRequestHook.CODE.equals(hookId.getHookImplCode())); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolver.java index f90ff7b6dcf..f3ff0bc65b5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingFlowResolver.java @@ -1,6 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.User; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; @@ -17,9 +20,13 @@ import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.LoggerFactory; -import org.prebid.server.settings.model.Account; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import java.util.Collection; import java.util.Objects; +import java.util.Optional; import java.util.Set; public class OptableTargetingFlowResolver { @@ -27,6 +34,8 @@ public class OptableTargetingFlowResolver { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); + private static final String OPTABLE_FIELD = "optable"; + private static final String IMP_STORED_REQUEST_PATH = "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/prebid/storedrequest"; private static final String AUCTION_NOT_PROPERLY_CONFIGURED = "Account not properly configured: tenant and/or origin is missing."; @@ -46,77 +55,173 @@ public OptableTargetingFlowResolver(BidderEnrichmentSampler bidderEnrichmentSamp this.logSamplingRate = logSamplingRate; } + /** + * Raw auction request stage. Stored requests and stored imps are merged only after it, so when the request + * relies on them the call is deferred to the processed auction request hook, which sees the merged request. + */ public Future> resolveAsyncOptableTargetingFlow( ModuleContext moduleContext, AuctionRequestPayload payload, AuctionInvocationContext invocationContext, - OptableTargetingProperties properties, - boolean cleanRequestOnFail) { + OptableTargetingProperties properties) { - final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest(); - if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties)) { - if (cleanRequestOnFail) { - moduleContext.setShouldSkipEnrichment(true); - } + final BidRequest bidRequest = payload.bidRequest(); + if (shouldDeferTargetingCall(bidRequest)) { moduleContext.setEarlyCallInitializationCompleted(false); - return cleanRequestOnFail - ? update(AuctionRequestCleaner.instance(), moduleContext) - : success(moduleContext); + // the cleaner strips user.ext.optable at this stage, while the deferred call still needs its ids + moduleContext.setExtUserOptable(extUserOptable(bidRequest)); + } else { + startTargetingCall(moduleContext, bidRequest, invocationContext, properties, true); + } + + return update(AuctionRequestCleaner.instance(), moduleContext); + } + + private void startDeferredTargetingCall(ModuleContext moduleContext, + AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties, + boolean awaitedByBidderRequestHook) { + + final BidRequest bidRequest = withExtUserOptable(payload.bidRequest(), moduleContext.getExtUserOptable()); + moduleContext.setEarlyCallInitializationCompleted(true); + moduleContext.setExtUserOptable(null); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + + startTargetingCall(moduleContext, bidRequest, invocationContext, properties, awaitedByBidderRequestHook); + } + + private boolean shouldDeferTargetingCall(BidRequest bidRequest) { + return (bidRequest.getSite() == null && bidRequest.getApp() == null) + || !bidderEnrichmentSampler.hasBidders(bidRequest) + || hasStoredRequest(bidRequest); + } + + private static boolean hasStoredRequest(BidRequest bidRequest) { + final ExtRequest ext = bidRequest.getExt(); + final ExtRequestPrebid prebid = ext != null ? ext.getPrebid() : null; + if (prebid != null && prebid.getStoredrequest() != null) { + return true; + } + + return Optional.ofNullable(bidRequest.getImp()) + .stream() + .flatMap(Collection::stream) + .map(Imp::getExt) + .filter(Objects::nonNull) + .anyMatch(impExt -> !impExt.at(IMP_STORED_REQUEST_PATH).isMissingNode()); + } + + private void startTargetingCall(ModuleContext moduleContext, + BidRequest bidRequest, + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties, + boolean awaitedByLaterHook) { + + if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties)) { + moduleContext.setShouldSkipEnrichment(true); + return; } final Set biddersToEnrich = bidderEnrichmentSampler.sample(bidRequest, properties); if (CollectionUtils.isEmpty(biddersToEnrich)) { - moduleContext.setEarlyCallInitializationCompleted(false); - return cleanRequestOnFail - ? update(AuctionRequestCleaner.instance(), moduleContext) - : success(moduleContext); + return; } - moduleContext.setBiddersToEnrich(biddersToEnrich); - final Account account = invocationContext.auctionContext().getAccount(); - final long crossHookFutureTimeout = - hooksExecutionPlan.getOptableTargetingBidderRequestTimeout(account); - final Future optableTargetingCall = targetingRequestExecutor.makeRequest( - payload, + bidRequest, invocationContext, properties, - crossHookFutureTimeout); + awaitedByLaterHook); + // set together, so that the bidder request hook never sees bidders without a call to await + moduleContext.setBiddersToEnrich(biddersToEnrich); moduleContext.setOptableTargetingCall(optableTargetingCall); + } - return update(AuctionRequestCleaner.instance(), moduleContext); + private static JsonNode extUserOptable(BidRequest bidRequest) { + final User user = bidRequest.getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + return extUser != null ? extUser.getProperty(OPTABLE_FIELD) : null; + } + + private static BidRequest withExtUserOptable(BidRequest bidRequest, JsonNode optable) { + if (optable == null) { + return bidRequest; + } + + final User user = bidRequest.getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + final ExtUser restoredExtUser = extUser != null ? extUser.toBuilder().build() : ExtUser.builder().build(); + if (extUser != null) { + restoredExtUser.addProperties(extUser.getProperties()); + } + restoredExtUser.addProperty(OPTABLE_FIELD, optable); + + final User restoredUser = (user != null ? user.toBuilder() : User.builder()).ext(restoredExtUser).build(); + return bidRequest.toBuilder().user(restoredUser).build(); } /** - * @deprecated This call is deprecated and will be removed in a future release. + * Processed auction request stage. */ - @Deprecated public Future> resolveOptableTargetingFlow( AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext, ModuleContext moduleContext, OptableTargetingProperties properties) { - if (moduleContext.isShouldSkipEnrichment()) { - moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); - return updateWithAnalytics(AuctionRequestCleaner.instance(), moduleContext); + final boolean hasBidderRequestHook = + hooksExecutionPlan.hasBidderRequestHook(invocationContext.auctionContext()); + + if (moduleContext.isEarlyNetworkCallEnabled()) { + final boolean deferred = !moduleContext.isEarlyCallInitializationCompleted(); + if (deferred) { + startDeferredTargetingCall( + moduleContext, auctionRequestPayload, invocationContext, properties, hasBidderRequestHook); + } + + if (hasBidderRequestHook) { + return deferred ? update(AuctionRequestCleaner.instance(), moduleContext) : noAction(moduleContext); + } + + return enrichWhenCompleted(moduleContext.getOptableTargetingCall(), moduleContext, properties); } - final Account account = invocationContext.auctionContext().getAccount(); - final boolean hasRawAuctionRequestHook = hooksExecutionPlan.hasRawAuctionRequestHook(account); - final boolean hasBidderRequestHook = hooksExecutionPlan.hasBidderRequestHook(account); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + moduleContext.setOptableTargetingProperties(properties); + if (!PropertiesValidator.isValid(properties)) { + conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate); + return failed(moduleContext); + } - if (hasRawAuctionRequestHook && hasBidderRequestHook) { + // the raw auction request stage does not run for f.e. amp and video requests, which are not enriched then + if (hasBidderRequestHook) { return updateWithAnalytics(AuctionRequestCleaner.instance(), moduleContext); } - final Future optableTargetingCall = hasRawAuctionRequestHook - ? resolveEarlyNetworkCall(moduleContext) - : resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties); + final BidRequest bidRequest = auctionRequestPayload.bidRequest(); + if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties)) { + moduleContext.setShouldSkipEnrichment(true); + return enrichWhenCompleted(null, moduleContext, properties); + } + + // the whole request is enriched here, so it is enriched for all bidders when any of them is sampled + final Future optableTargetingCall = + CollectionUtils.isNotEmpty(bidderEnrichmentSampler.sample(bidRequest, properties)) + ? targetingRequestExecutor.makeRequest(bidRequest, invocationContext, properties, false) + : null; + + return enrichWhenCompleted(optableTargetingCall, moduleContext, properties); + } - if (optableTargetingCall == null) { - moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext)); + private Future> enrichWhenCompleted( + Future optableTargetingCall, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + if (moduleContext.isShouldSkipEnrichment() || optableTargetingCall == null) { + moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); return updateWithAnalytics(AuctionRequestCleaner.instance(), moduleContext); } @@ -146,36 +251,10 @@ private Future> enrichPayload( return updateWithAnalytics(payloadUpdate, moduleContext); } - private Future resolveEarlyNetworkCall(ModuleContext moduleContext) { - return moduleContext.getOptableTargetingCall(); - } - private static long calcAPICallExecutionTime(ModuleContext moduleContext) { return System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp(); } - private Future resolvePreEarlyNetworkCall( - AuctionRequestPayload payload, - AuctionInvocationContext invocationContext, - ModuleContext moduleContext, - OptableTargetingProperties properties) { - - 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 targetingRequestExecutor.makeRequest( - payload, - invocationContext, - properties, - null); - } - private static Future> update( PayloadUpdate payloadUpdate, ModuleContext moduleContext) { @@ -189,26 +268,31 @@ private static Future> update( .build()); } - private static Future> updateWithAnalytics( - PayloadUpdate payloadUpdate, - ModuleContext moduleContext) { - + private static Future> noAction(ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) - .action(InvocationAction.update) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) - .payloadUpdate(payloadUpdate) + .action(InvocationAction.no_action) .moduleContext(moduleContext) .build()); } - public static Future> success(ModuleContext moduleContext) { + public Future> failed(ModuleContext moduleContext) { + moduleContext.failWithExecutionTime( + moduleContext.getCallTargetingAPITimestamp() > 0 ? calcAPICallExecutionTime(moduleContext) : 0); + return updateWithAnalytics(AuctionRequestCleaner.instance(), moduleContext); + } + + private static Future> updateWithAnalytics( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) - .action(InvocationAction.no_action) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) .build()); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 8a93df9fbdf..bc73cbdaee2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java @@ -12,6 +12,7 @@ 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.model.TimeoutContext; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; @@ -20,7 +21,6 @@ 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; @@ -42,26 +42,38 @@ public TargetingRequestExecutor(OptableTargeting optableTargeting, this.logSamplingRate = logSamplingRate; } - public Future makeRequest(AuctionRequestPayload payload, + public Future makeRequest(BidRequest bidRequest, AuctionInvocationContext invocationContext, OptableTargetingProperties properties, - Long apiTimeout) { + boolean awaitedByLaterHook) { - final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext); + final BidRequest restrictedBidRequest = applyActivityRestrictions(bidRequest, invocationContext); - final Timeout timeout = apiTimeout == null - ? getHookTimeout(invocationContext) - : timeoutFactory.create(getHookTimeout(invocationContext).remaining() + apiTimeout); + final Timeout timeout = awaitedByLaterHook + ? resolveCrossHookTimeout(invocationContext, properties) + : invocationContext.timeout(); final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), properties.getTimeout(), logSamplingRate); - return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout); + return optableTargeting.getTargeting(properties, restrictedBidRequest, attributes, timeout); } - private static Timeout getHookTimeout(AuctionInvocationContext invocationContext) { - return invocationContext.timeout(); + /** + * A call that is awaited by a later hook can't be bound by the timeout of the hook that starts it. + */ + private Timeout resolveCrossHookTimeout(AuctionInvocationContext invocationContext, + OptableTargetingProperties properties) { + + final Long apiTimeout = properties.getApiTimeout(); + if (apiTimeout != null && apiTimeout > 0) { + return timeoutFactory.create(apiTimeout); + } + + final TimeoutContext timeoutContext = invocationContext.auctionContext().getTimeoutContext(); + final Timeout auctionTimeout = timeoutContext != null ? timeoutContext.getTimeout() : null; + return auctionTimeout != null ? auctionTimeout : invocationContext.timeout(); } private BidRequest applyActivityRestrictions(BidRequest bidRequest, 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 386055b6c84..a373e6283a5 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 @@ -27,6 +27,13 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; 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.HookHttpEndpoint; +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.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Query; @@ -123,6 +130,15 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr return givenAuctionContext(activityInfrastructure, timeout, null); } + protected static ExecutionPlan givenBidderRequestHookPlan() { + final StageExecutionPlan bidderRequestStage = StageExecutionPlan.of(List.of(ExecutionGroup.of( + null, List.of(HookId.of("optable-targeting", "optable-targeting-bidder-request-hook"))))); + + return ExecutionPlan.of(null, Map.of( + HookHttpEndpoint.POST_AUCTION, + EndpointExecutionPlan.of(Map.of(Stage.bidder_request, bidderRequestStage)))); + } + protected HttpRequestContext givenHttpRequestContext() { return givenHttpRequestContext(null); } 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 index 6d527b0f663..997b36a85c3 100644 --- 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 @@ -17,13 +17,17 @@ import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.model.ExecutionPlan; 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.AuctionRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; 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.OptableTargetingFlowResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; 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; @@ -32,6 +36,7 @@ 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.mock; import static org.mockito.Mockito.when; @MockitoSettings(strictness = Strictness.LENIENT) @@ -72,13 +77,14 @@ public void setUp() { when(invocationContext.timeout()).thenReturn(timeout); when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); when(timeout.remaining()).thenReturn(1000L); + when(bidderEnrichmentSampler.hasBidders(any())).thenReturn(true); } private OptableTargetingFlowResolver givenEarlyOptableCallResolver() { return new OptableTargetingFlowResolver( bidderEnrichmentSampler, targetingRequestExecutor, - CompositeHookExecutionPlan.of(ExecutionPlan.empty()), + CompositeHookExecutionPlan.of(ExecutionPlan.empty(), ExecutionPlan.empty()), 0.01); } @@ -212,9 +218,28 @@ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenNoBiddersToEnrich( final ModuleContext moduleContext = cxt.result(); assertThat(moduleContext.isShouldSkipEnrichment()).isFalse(); assertThat(moduleContext.getOptableTargetingCall()).isNull(); - assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); }); vertxTestContext.completeNow(); }); } + + @Test + public void shouldCleanRequestWhenInitiatingCallFails() { + // given + final ConfigResolver failingConfigResolver = mock(ConfigResolver.class); + when(failingConfigResolver.resolve(any())).thenThrow(new IllegalStateException("failure")); + target = new OptableRawAuctionRequestHook(failingConfigResolver, givenEarlyOptableCallResolver(), 0.01); + + // when + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); + + // then + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate()).isInstanceOf(AuctionRequestCleaner.class); + assertThat(((ModuleContext) result.moduleContext()).getEnrichRequestStatus().getStatus()) + .isEqualTo(Status.FAIL); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableStoredRequestFlowTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableStoredRequestFlowTest.java new file mode 100644 index 00000000000..d0c26892ba6 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableStoredRequestFlowTest.java @@ -0,0 +1,323 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +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.ArgumentCaptor; +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.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +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.v1.core.AliasesResolver; +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.CompositeHookExecutionPlan; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargetingFlowResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +import java.util.List; +import java.util.Map; +import java.util.function.IntSupplier; + +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.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +/** + * Walks a request through the raw auction request, processed auction request and bidder request hooks the way + * the core does for requests that rely on stored requests: the raw stage sees only stored request ids, the later + * stages see the merged request. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class OptableStoredRequestFlowTest extends BaseOptableTest { + + @Mock + private OptableTargeting optableTargeting; + @Mock + private UserFpdActivityMask userFpdActivityMask; + @Mock + private ActivityInfrastructure activityInfrastructure; + @Mock + private AuctionRequestPayload rawPayload; + @Mock + private AuctionInvocationContext rawInvocationContext; + @Mock + private AuctionInvocationContext processedInvocationContext; + @Mock + private BidderInvocationContext bidderInvocationContext; + @Mock + private Timeout timeout; + @Mock + private TimeoutFactory timeoutFactory; + @Mock + private BidderCatalog bidderCatalog; + @Mock + private IntSupplier randomSupplier; + + private OptableTargetingProperties properties; + + private OptableRawAuctionRequestHook rawHook; + private OptableTargetingProcessedAuctionRequestHook processedHook; + private OptableBidderRequestHook bidderHook; + private InvocationResult rawResult; + + @BeforeEach + public void setUp() { + when(userFpdActivityMask.maskUser(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); + when(timeout.remaining()).thenReturn(1000L); + when(rawInvocationContext.timeout()).thenReturn(timeout); + when(rawInvocationContext.accountConfig()).thenAnswer(invocation -> mapper.valueToTree(properties)); + when(processedInvocationContext.timeout()).thenReturn(timeout); + when(processedInvocationContext.accountConfig()).thenAnswer(invocation -> mapper.valueToTree(properties)); + when(bidderInvocationContext.timeout()).thenReturn(timeout); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + properties = givenOptableTargetingProperties(false); + properties.setEnrichApp(true); + + final OptableTargetingFlowResolver flowResolver = new OptableTargetingFlowResolver( + BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog), randomSupplier), + new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory, 0.01), + CompositeHookExecutionPlan.of(givenBidderRequestHookPlan(), ExecutionPlan.empty()), + 0.01); + final ConfigResolver configResolver = new ConfigResolver(mapper, jsonMerger, properties); + rawHook = new OptableRawAuctionRequestHook(configResolver, flowResolver, 0.01); + processedHook = new OptableTargetingProcessedAuctionRequestHook(configResolver, flowResolver, 0.01); + bidderHook = new OptableBidderRequestHook(); + } + + @Test + public void shouldEnrichBiddersThatComeFromStoredImps() { + // given + final BidRequest rawRequest = givenBidRequest().toBuilder().imp(List.of(givenStoredImp())).build(); + + // when + final ModuleContext moduleContext = callRawHook(rawRequest); + + // then + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + verifyNoInteractions(optableTargeting); + + // when + final BidRequest mergedRequest = givenCleaned(rawRequest).toBuilder() + .imp(List.of(givenImp("bidderA", "bidderB"))) + .build(); + callProcessedHook(mergedRequest, moduleContext); + final InvocationResult bidderAResult = + callBidderHook("bidderA", mergedRequest, moduleContext); + final InvocationResult bidderBResult = + callBidderHook("bidderB", mergedRequest, moduleContext); + + // then + assertThat(bidderAResult.action()).isEqualTo(InvocationAction.update); + assertThat(bidderBResult.action()).isEqualTo(InvocationAction.update); + assertThat(moduleContext.getBiddersToEnrich()).containsExactlyInAnyOrder("bidderA", "bidderB"); + + final ArgumentCaptor captor = ArgumentCaptor.forClass(BidRequest.class); + verify(optableTargeting, times(1)).getTargeting(any(), captor.capture(), any(), any()); + final ObjectNode optable = (ObjectNode) captor.getValue().getUser().getExt().getProperty("optable"); + assertThat(optable.get("email").asText()).isEqualTo("email"); + assertThat(mergedRequest.getUser().getExt().getProperty("optable")).isNull(); + + final BidRequest enriched = bidderAResult.payloadUpdate() + .apply(BidderRequestPayloadImpl.of(mergedRequest)) + .bidRequest(); + assertThat(enriched.getUser().getEids()).isNotEmpty(); + } + + @Test + public void shouldEnrichWhenAppComesFromStoredRequest() { + // given + final BidRequest rawRequest = givenBidRequest().toBuilder() + .site(null) + .imp(List.of(givenImp("bidderA"))) + .build(); + + // when + final ModuleContext moduleContext = callRawHook(rawRequest); + + // then + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + assertThat(moduleContext.isShouldSkipEnrichment()).isFalse(); + + // when + final BidRequest mergedRequest = givenCleaned(rawRequest).toBuilder() + .app(App.builder().bundle("bundle").build()) + .build(); + callProcessedHook(mergedRequest, moduleContext); + final InvocationResult result = + callBidderHook("bidderA", mergedRequest, moduleContext); + + // then + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(moduleContext.isShouldSkipEnrichment()).isFalse(); + verify(optableTargeting).getTargeting(any(), any(), any(), any()); + } + + @Test + public void shouldHonourDisabledTrafficSourceOnMergedRequest() { + // given + properties.setEnrichApp(false); + final BidRequest rawRequest = givenBidRequest().toBuilder() + .site(null) + .imp(List.of(givenImp("bidderA"))) + .build(); + + // when + final ModuleContext moduleContext = callRawHook(rawRequest); + final BidRequest mergedRequest = givenCleaned(rawRequest).toBuilder() + .app(App.builder().bundle("bundle").build()) + .build(); + callProcessedHook(mergedRequest, moduleContext); + final InvocationResult result = + callBidderHook("bidderA", mergedRequest, moduleContext); + + // then + assertThat(result.payloadUpdate()).isInstanceOf(BidRequestCleaner.class); + assertThat(moduleContext.isShouldSkipEnrichment()).isTrue(); + verifyNoInteractions(optableTargeting); + } + + @Test + public void shouldNotSampleAgainWhenSamplingRejectedAllBiddersAtRawStage() { + // given + properties.setEnrichmentPercentage(10); + when(randomSupplier.getAsInt()).thenReturn(50, 0); + final BidRequest rawRequest = givenBidRequest().toBuilder().imp(List.of(givenImp("bidderA"))).build(); + + // when + final ModuleContext moduleContext = callRawHook(rawRequest); + callProcessedHook(givenCleaned(rawRequest), moduleContext); + final InvocationResult result = + callBidderHook("bidderA", givenCleaned(rawRequest), moduleContext); + + // then + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); + assertThat(result.payloadUpdate()).isInstanceOf(BidRequestCleaner.class); + verify(randomSupplier, times(1)).getAsInt(); + verifyNoInteractions(optableTargeting); + } + + @Test + public void shouldDeferWhenOnlySomeImpsRelyOnStoredImps() { + // given + final BidRequest rawRequest = givenBidRequest().toBuilder() + .imp(List.of(givenImp("bidderA"), givenStoredImp())) + .build(); + + // when + final ModuleContext moduleContext = callRawHook(rawRequest); + + // then + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + verifyNoInteractions(optableTargeting); + + // when + final BidRequest mergedRequest = givenCleaned(rawRequest).toBuilder() + .imp(List.of(givenImp("bidderA"), givenImp("bidderB"))) + .build(); + callProcessedHook(mergedRequest, moduleContext); + final InvocationResult bidderBResult = + callBidderHook("bidderB", mergedRequest, moduleContext); + + // then + assertThat(moduleContext.getBiddersToEnrich()).containsExactlyInAnyOrder("bidderA", "bidderB"); + assertThat(bidderBResult.action()).isEqualTo(InvocationAction.update); + } + + @Test + public void shouldCleanRequestAtRawStageWhenCallIsDeferred() { + // given + final BidRequest rawRequest = givenBidRequest().toBuilder().imp(List.of(givenStoredImp())).build(); + + // when + callRawHook(rawRequest); + + // then + assertThat(rawResult.action()).isEqualTo(InvocationAction.update); + assertThat(givenCleaned(rawRequest).getUser().getExt().getProperty("optable")).isNull(); + } + + private void callProcessedHook(BidRequest mergedRequest, ModuleContext moduleContext) { + when(processedInvocationContext.moduleContext()).thenReturn(moduleContext); + when(processedInvocationContext.auctionContext()).thenReturn(givenAuctionContext(mergedRequest)); + + processedHook.call(AuctionRequestPayloadImpl.of(mergedRequest), processedInvocationContext).result(); + } + + private ModuleContext callRawHook(BidRequest bidRequest) { + when(rawPayload.bidRequest()).thenReturn(bidRequest); + when(rawInvocationContext.auctionContext()).thenReturn(givenAuctionContext(bidRequest)); + + rawResult = rawHook.call(rawPayload, rawInvocationContext).result(); + return (ModuleContext) rawResult.moduleContext(); + } + + private InvocationResult callBidderHook(String bidder, + BidRequest mergedRequest, + ModuleContext moduleContext) { + + when(bidderInvocationContext.bidder()).thenReturn(bidder); + when(bidderInvocationContext.moduleContext()).thenReturn(moduleContext); + when(bidderInvocationContext.auctionContext()).thenReturn(givenAuctionContext(mergedRequest)); + + return bidderHook.call(BidderRequestPayloadImpl.of(mergedRequest), bidderInvocationContext).result(); + } + + private AuctionContext givenAuctionContext(BidRequest bidRequest) { + return givenAuctionContext(activityInfrastructure, timeout).toBuilder().bidRequest(bidRequest).build(); + } + + private BidRequest givenCleaned(BidRequest bidRequest) { + return rawResult.payloadUpdate().apply(AuctionRequestPayloadImpl.of(bidRequest)).bidRequest(); + } + + private Imp givenStoredImp() { + final ObjectNode ext = mapper.createObjectNode(); + ext.putObject("prebid").putObject("storedrequest").put("id", "storedImpId"); + return Imp.builder().id("impId").ext(ext).build(); + } + + private Imp givenImp(String... bidders) { + final ObjectNode ext = mapper.createObjectNode(); + final ObjectNode bidderNode = ext.putObject("prebid").putObject("bidder"); + for (String bidder : bidders) { + bidderNode.set(bidder, mapper.valueToTree(Map.of("param", "value"))); + } + return Imp.builder().id("impId").ext(ext).build(); + } +} 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 a942f570803..558ca437ad3 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 @@ -26,7 +26,9 @@ 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.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -47,6 +49,8 @@ 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.mock; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -90,7 +94,7 @@ void setUp() { targetingRequestExecutor = new TargetingRequestExecutor( optableTargeting, userFpdActivityMask, timeoutFactory, 0.01); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, givenFlowResolver(ExecutionPlan.empty())); + configResolver, givenFlowResolver(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn( @@ -98,13 +102,14 @@ void setUp() { when(invocationContext.timeout()).thenReturn(timeout); when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); when(timeout.remaining()).thenReturn(1000L); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder")); } private OptableTargetingFlowResolver givenFlowResolver(ExecutionPlan executionPlan) { return new OptableTargetingFlowResolver( bidderEnrichmentSampler, targetingRequestExecutor, - CompositeHookExecutionPlan.of(executionPlan), + CompositeHookExecutionPlan.of(executionPlan, ExecutionPlan.empty()), 0.01); } @@ -217,41 +222,39 @@ void callShouldLeaveId5SignatureNullWhenTargetingResultHasNoId5Signature() { } @Test - void callShouldReturnResultWithNoActionWhenEarlyOptableCallIsEnabledAndInitialized() { + void callShouldEnrichRequestWithEarlyCallResultWhenBidderRequestHookIsAbsent() { // given final ModuleContext moduleContext = new ModuleContext(); moduleContext.setEarlyNetworkCallEnabled(true); moduleContext.setEarlyCallInitializationCompleted(true); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); target = new OptableTargetingProcessedAuctionRequestHook( configResolver, - givenFlowResolver(givenExecutionPlan(true, false))); + givenFlowResolver(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( targetingRequestExecutor.makeRequest( - auctionRequestPayload, + givenBidRequest(), invocationContext, givenOptableTargetingProperties("key", "tenant", "origin", false), - null)); + false)); // when - final Future> future = target.call(auctionRequestPayload, - invocationContext); + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); // then - assertThat(future).isNotNull(); - assertThat(future.succeeded()).isTrue(); - - final InvocationResult result = future.result(); - assertThat(result).isNotNull() - .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action) - .extracting(InvocationResult::errors).isNull(); - assertThat(result.payloadUpdate()).isNull(); - assertThat(moduleContext.getOptableTargetingCall()).isNotNull(); - assertThat(moduleContext.getOptableTargetingCall().succeeded()).isTrue(); + assertThat(result.action()).isEqualTo(InvocationAction.update); + final BidRequest bidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getEids()) + .flatExtracting(Eid::getUids) + .extracting(Uid::getId) + .containsExactly("id"); } @Test @@ -260,6 +263,9 @@ void callShouldRetryEarlyNetworkCallInitializationWhenItWasNotCompletedOnRawAuct final ModuleContext moduleContext = new ModuleContext(); moduleContext.setEarlyNetworkCallEnabled(true); moduleContext.setEarlyCallInitializationCompleted(false); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + givenFlowResolver(givenExecutionPlan(true, true)), 0.01); final TargetingResult targetingResult = givenTargetingResult(); when(invocationContext.moduleContext()).thenReturn(moduleContext); @@ -301,8 +307,12 @@ void callShouldNotRetryEarlyNetworkCallInitializationWhenNoBiddersToEnrich() { final ModuleContext moduleContext = new ModuleContext(); moduleContext.setEarlyNetworkCallEnabled(true); moduleContext.setEarlyCallInitializationCompleted(false); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + givenFlowResolver(givenExecutionPlan(true, true)), 0.01); when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of()); // when @@ -319,7 +329,7 @@ void callShouldNotRetryEarlyNetworkCallInitializationWhenNoBiddersToEnrich() { .returns(InvocationAction.update, InvocationResult::action) .extracting(InvocationResult::errors).isNull(); assertThat(moduleContext.getOptableTargetingCall()).isNull(); - assertThat(moduleContext.isEarlyCallInitializationCompleted()).isFalse(); + assertThat(moduleContext.isEarlyCallInitializationCompleted()).isTrue(); } @Test @@ -327,7 +337,7 @@ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { // given target = new OptableTargetingProcessedAuctionRequestHook( configResolver, - givenFlowResolver(givenExecutionPlan(false, false))); + givenFlowResolver(givenExecutionPlan(false, false)), 0.01); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); @@ -360,69 +370,41 @@ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { } @Test - void callShouldReturnResultWithEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() { + void callShouldOnlyCleanRequestWhenRawAuctionRequestHookDidNotRunAndBidderRequestHookIsPresent() { // given target = new OptableTargetingProcessedAuctionRequestHook( configResolver, - givenFlowResolver(givenExecutionPlan(false, true))); - when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), any(), any())) - .thenReturn(Future.succeededFuture(givenTargetingResult())); + givenFlowResolver(givenExecutionPlan(false, true)), 0.01); // when - final Future> future = target.call(auctionRequestPayload, - invocationContext); + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); // 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"); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate()).isInstanceOf(AuctionRequestCleaner.class); + assertThat(((ModuleContext) result.moduleContext()).getOptableTargetingCall()).isNull(); + verifyNoInteractions(optableTargeting); } @Test - void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { + void callShouldReturnNoActionWhenRawAuctionRequestHookStartedCallForBidderRequestHook() { // given target = new OptableTargetingProcessedAuctionRequestHook( configResolver, - givenFlowResolver(givenExecutionPlan(true, true))); - when(invocationContext.moduleContext()).thenReturn(new ModuleContext()); + givenFlowResolver(givenExecutionPlan(true, true)), 0.01); + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyNetworkCallEnabled(true); + when(invocationContext.moduleContext()).thenReturn(moduleContext); // when - final Future> future = target.call(auctionRequestPayload, - invocationContext); + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); // 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()).isNull(); - assertThat(bidRequest.getUser().getData()).isNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.payloadUpdate()).isNull(); } @Test @@ -433,7 +415,7 @@ void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, givenFlowResolver(ExecutionPlan.empty())); + configResolver, givenFlowResolver(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -462,7 +444,7 @@ void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { jsonMerger, givenOptableTargetingProperties("key", null, "origin", false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, givenFlowResolver(ExecutionPlan.empty())); + configResolver, givenFlowResolver(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); @@ -537,6 +519,7 @@ void callShouldReturnResultWithUpdateWhenOptableTargetingDoesNotReturnResult() { void callShouldReturnUpdateWhenTrafficSourceIsInvalid() { // given final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyNetworkCallEnabled(true); moduleContext.setShouldSkipEnrichment(true); when(invocationContext.moduleContext()).thenReturn(moduleContext); @@ -577,4 +560,63 @@ private ExecutionPlan givenExecutionPlan(boolean hasRawAuctionRequestHook, boole return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_AUCTION, endpointExecutionPlan)); } + + @Test + void callShouldCleanRequestWhenResolvingFlowFails() { + // given + final ConfigResolver failingConfigResolver = mock(ConfigResolver.class); + when(failingConfigResolver.resolve(any())).thenThrow(new IllegalStateException("failure")); + target = new OptableTargetingProcessedAuctionRequestHook( + failingConfigResolver, givenFlowResolver(ExecutionPlan.empty()), 0.01); + + // when + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); + + // then + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.payloadUpdate()).isInstanceOf(AuctionRequestCleaner.class); + assertThat(((ModuleContext) result.moduleContext()).getEnrichRequestStatus().getStatus()) + .isEqualTo(Status.FAIL); + } + + @Test + void callShouldNotCallApiInLegacyModeWhenTrafficSourceIsDisabled() { + // given + final OptableTargetingProperties properties = givenOptableTargetingProperties(false); + properties.setEnrichWeb(false); + target = new OptableTargetingProcessedAuctionRequestHook( + new ConfigResolver(mapper, jsonMerger, properties), givenFlowResolver(ExecutionPlan.empty()), 0.01); + when(invocationContext.accountConfig()).thenReturn(null); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + + // when + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); + + // then + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(((ModuleContext) result.moduleContext()).isShouldSkipEnrichment()).isTrue(); + verifyNoInteractions(optableTargeting); + } + + @Test + void callShouldNotCallApiInLegacyModeWhenNoBidderIsSampled() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of()); + + // when + final InvocationResult result = + target.call(auctionRequestPayload, invocationContext).result(); + + // then + assertThat(result.action()).isEqualTo(InvocationAction.update); + final BidRequest bidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getEids()).isNull(); + verifyNoInteractions(optableTargeting); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java index 606c89ee102..dc9d07322ad 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java @@ -3,9 +3,9 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.User; import org.junit.jupiter.api.Test; -import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; -import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import static org.assertj.core.api.Assertions.assertThat; @@ -14,14 +14,14 @@ public class BidRequestCleanerTest extends BaseOptableTest { @Test public void shouldRemoveUserExtOptableTag() { // given - final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> + final BidderRequestPayload bidderRequestPayload = BidderRequestPayloadImpl.of(givenBidRequest(bidRequest -> bidRequest.user(givenUser()))); // when - final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload); + final BidderRequestPayload result = BidRequestCleaner.instance().apply(bidderRequestPayload); // then - assertThat(result).extracting(AuctionRequestPayload::bidRequest) + assertThat(result).extracting(BidderRequestPayload::bidRequest) .extracting(BidRequest::getUser) .extracting(User::getExt) .extracting(it -> it.getProperty("optable")) @@ -36,14 +36,14 @@ public void shouldKeepOtherUserExtOptableTags() { .put("other", "value") .put("id5_signature", "signature"); - final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> + final BidderRequestPayload bidderRequestPayload = BidderRequestPayloadImpl.of(givenBidRequest(bidRequest -> bidRequest.user(user))); // when - final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload); + final BidderRequestPayload result = BidRequestCleaner.instance().apply(bidderRequestPayload); // then - assertThat(result).extracting(AuctionRequestPayload::bidRequest) + assertThat(result).extracting(BidderRequestPayload::bidRequest) .extracting(BidRequest::getUser) .extracting(User::getExt) .extracting(it -> (com.fasterxml.jackson.databind.node.ObjectNode) it.getProperty("optable")) 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 index c836de388a9..09b2b728b51 100644 --- 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 @@ -1,9 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.junit.jupiter.api.Test; +import org.prebid.server.auction.model.AuctionContext; 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.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookHttpEndpoint; import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.Stage; @@ -16,345 +18,89 @@ import static org.assertj.core.api.Assertions.assertThat; -public class CompositeHookExecutionPlanTest { +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(); - } + private static final HookId BIDDER_REQUEST_HOOK = + HookId.of("optable-targeting", "optable-targeting-bidder-request-hook"); @Test - public void hasBidderRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() { + void hasBidderRequestHookShouldFindHookInHostPlan() { // given - final ExecutionPlan accountPlan = givenExecutionPlan( - "bidder_request", "optable-targeting-bidder-request-hook"); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); - final Account account = givenAccount("accountId", accountPlan); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of( + givenPlan(HookHttpEndpoint.POST_AUCTION, BIDDER_REQUEST_HOOK), null); // when and then - assertThat(target.hasBidderRequestHook(account)).isTrue(); - assertThat(target.hasBidderRequestHook(account)).isTrue(); + assertThat(target.hasBidderRequestHook(givenAuctionContext(HookHttpEndpoint.POST_AUCTION, null))).isTrue(); } @Test - public void hasRawAuctionRequestHookShouldReturnFalseWhenOnlyBidderRequestHookIsInGlobalPlan() { + void hasBidderRequestHookShouldFindHookInAccountPlan() { // 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(); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, null); + final ExecutionPlan accountPlan = givenPlan(HookHttpEndpoint.POST_AUCTION, BIDDER_REQUEST_HOOK); // when and then - assertThat(target.hasRawAuctionRequestHook(account)).isFalse(); + assertThat(target.hasBidderRequestHook(givenAuctionContext(HookHttpEndpoint.POST_AUCTION, accountPlan))) + .isTrue(); } @Test - public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIsInGlobalPlan() { + void hasBidderRequestHookShouldFallBackToDefaultAccountPlanWhenAccountHasNone() { // 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(); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of( + null, givenPlan(HookHttpEndpoint.POST_AUCTION, BIDDER_REQUEST_HOOK)); // when and then - assertThat(target.hasBidderRequestHook(account)).isFalse(); + assertThat(target.hasBidderRequestHook(givenAuctionContext(HookHttpEndpoint.POST_AUCTION, null))).isTrue(); } @Test - public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenConfigured() { + void hasBidderRequestHookShouldIgnoreDefaultAccountPlanWhenAccountHasOne() { // given - final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 500L); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); - final Account account = Account.builder().id("accountId").build(); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of( + null, givenPlan(HookHttpEndpoint.POST_AUCTION, BIDDER_REQUEST_HOOK)); // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(500L); + assertThat(target.hasBidderRequestHook( + givenAuctionContext(HookHttpEndpoint.POST_AUCTION, ExecutionPlan.empty()))).isFalse(); } @Test - public void getBidderRequestTimeoutShouldReturnAccountTimeoutWhenAccountPlanOverrides() { + void hasBidderRequestHookShouldLookAtEndpointOfRequest() { // given - final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 500L); - final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 200L); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); - final Account account = givenAccount("accountId", accountPlan); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of( + givenPlan(HookHttpEndpoint.POST_AUCTION, BIDDER_REQUEST_HOOK), null); // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(200L); + assertThat(target.hasBidderRequestHook(givenAuctionContext(HookHttpEndpoint.AMP, null))).isFalse(); } @Test - public void getBidderRequestTimeoutShouldFallbackToGlobalWhenAccountPlanHasNoTimeout() { + void hasBidderRequestHookShouldFindHookInAnyGroupOfStage() { // given - final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 300L); - 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); + final StageExecutionPlan stage = StageExecutionPlan.of(List.of( + ExecutionGroup.of(10L, List.of(HookId.of("other", "other-hook"))), + ExecutionGroup.of(20L, List.of(BIDDER_REQUEST_HOOK)))); + final ExecutionPlan plan = ExecutionPlan.of(null, Map.of( + HookHttpEndpoint.AMP, EndpointExecutionPlan.of(Map.of(Stage.bidder_request, stage)))); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(plan, null); // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(300L); + assertThat(target.hasBidderRequestHook(givenAuctionContext(HookHttpEndpoint.AMP, null))).isTrue(); } - @Test - public void getBidderRequestTimeoutShouldReturnZeroWhenNoPlanIsConfigured() { - // given - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); - final Account account = Account.builder().id("accountId").build(); - - // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(0L); + private static ExecutionPlan givenPlan(HookHttpEndpoint endpoint, HookId hookId) { + final StageExecutionPlan stage = StageExecutionPlan.of(List.of(ExecutionGroup.of(10L, List.of(hookId)))); + return ExecutionPlan.of(null, Map.of(endpoint, EndpointExecutionPlan.of(Map.of(Stage.bidder_request, stage)))); } - @Test - public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIsNull() { - // given - final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 400L); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); - - // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(null)).isEqualTo(400L); - } - - @Test - public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIdIsEmpty() { - // given - final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 150L); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); - final Account account = Account.builder().id("").build(); - - // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(150L); - } - - @Test - public void getBidderRequestTimeoutShouldReturnSameResultOnRepeatedCallsForSameAccount() { - // given - final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout( - "bidder_request", - "optable-targeting-bidder-request-hook", - 250L); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); - final Account account = givenAccount("accountId", accountPlan); - - // when and then - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(250L); - assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(250L); - } - - 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(HookHttpEndpoint.POST_AUCTION, endpointPlan)); - } - - private ExecutionPlan givenExecutionPlanWithTimeout(String stage, String hookCode, long timeout) { - final HookId hookId = HookId.of("optable-targeting", hookCode); - final ExecutionGroup group = ExecutionGroup.of(timeout, 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(HookHttpEndpoint.POST_AUCTION, endpointPlan)); - } - - private Account givenAccount(String accountId, ExecutionPlan executionPlan) { - return Account.builder() - .id(accountId) - .hooks(AccountHooksConfiguration.of(executionPlan, null, null)) + private static AuctionContext givenAuctionContext(HookHttpEndpoint endpoint, ExecutionPlan accountPlan) { + return AuctionContext.builder() + .hookExecutionContext(HookExecutionContext.of(endpoint)) + .account(Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of(accountPlan, null, null)) + .build()) .build(); } } -