diff --git a/docs/application-settings.md b/docs/application-settings.md index 7f164caa0dc..2a20e0d8342 100644 --- a/docs/application-settings.md +++ b/docs/application-settings.md @@ -101,6 +101,7 @@ Keep in mind following restrictions: - `cookie-sync.pri` - a list of prioritized bidder codes - `cookie-sync.coop-sync.default` - if the "coopSync" value isn't specified in the `/cookie_sync` request, use this - `hooks` - configuration for Prebid Server Modules. For further details, see: https://docs.prebid.org/prebid-server/pbs-modules/index.html#2-define-an-execution-plan +- `hooks.admin.module-execution` - a key-value map, where a key is a module name and a value is a boolean, that defines whether modules hooks should/should not be always executed; if the module is not specified it is executed by default when it's present in the execution plan - `settings.geo-lookup` - enables geo lookup for account if true. Defaults to false. Here are the definitions of the "purposes" that can be defined in the GDPR setting configurations: diff --git a/docs/config-app.md b/docs/config-app.md index 40a2c42784e..37bb87462f6 100644 --- a/docs/config-app.md +++ b/docs/config-app.md @@ -369,6 +369,9 @@ contain 'WHERE last_updated > ?' for MySQL and 'WHERE last_updated > $1' for Pos For targeting available next options: - `settings.targeting.truncate-attr-chars` - set the max length for names of targeting keywords (0 means no truncation). +For modules: +- `settings.modules.require-config-to-invoke` - when enabled it requires a runtime config to exist for a module. + ## Host Cookie - `host-cookie.optout-cookie.name` - set the cookie name for optout checking. - `host-cookie.optout-cookie.value` - set the cookie value for optout checking. diff --git a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java index 5fac3d8376b..6ec0cc63095 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.execution; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.core.AsyncResult; import io.vertx.core.Future; import io.vertx.core.Promise; @@ -9,16 +8,13 @@ import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.v1.Hook; -import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.LoggerFactory; import java.time.Clock; +import java.util.Map; import java.util.concurrent.TimeoutException; import java.util.function.Function; import java.util.function.Supplier; @@ -30,7 +26,7 @@ class GroupExecutor { private final Vertx vertx; private final Clock clock; - private final boolean isConfigToInvokeRequired; + private final Map modulesExecution; private ExecutionGroup group; private PAYLOAD initialPayload; @@ -39,18 +35,19 @@ class GroupExecutor { private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; - private GroupExecutor(Vertx vertx, Clock clock, boolean isConfigToInvokeRequired) { + private GroupExecutor(Vertx vertx, Clock clock, Map modulesExecution) { + this.vertx = vertx; this.clock = clock; - this.isConfigToInvokeRequired = isConfigToInvokeRequired; + this.modulesExecution = modulesExecution; } public static GroupExecutor create( Vertx vertx, Clock clock, - boolean isConfigToInvokeRequired) { + Map modulesExecution) { - return new GroupExecutor<>(vertx, clock, isConfigToInvokeRequired); + return new GroupExecutor<>(vertx, clock, modulesExecution); } public GroupExecutor withGroup(ExecutionGroup group) { @@ -90,6 +87,10 @@ public Future> execute() { Future> groupFuture = Future.succeededFuture(initialGroupResult); for (final HookId hookId : group.getHookSequence()) { + if (!modulesExecution.getOrDefault(hookId.getModuleCode(), true)) { + continue; + } + final Hook hook = hookProvider.apply(hookId); final long startTime = clock.millis(); @@ -116,17 +117,6 @@ private Future> executeHook( } final CONTEXT invocationContext = invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId)); - - if (isConfigToInvokeRequired && invocationContext instanceof AuctionInvocationContext) { - final ObjectNode accountConfig = ((AuctionInvocationContext) invocationContext).accountConfig(); - if (accountConfig == null || accountConfig.isNull()) { - return Future.succeededFuture(InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_invocation) - .build()); - } - } - return executeWithTimeout(() -> hook.call(groupResult.payload(), invocationContext), timeout); } diff --git a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java index 8e565d29d90..534205225b3 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -5,6 +5,7 @@ import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; import io.vertx.core.Vertx; +import org.apache.commons.collections4.map.DefaultedMap; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.model.AuctionContext; @@ -46,14 +47,17 @@ import org.prebid.server.model.CaseInsensitiveMultiMap; import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.HooksAdminConfig; import org.prebid.server.settings.model.AccountHooksConfiguration; import java.time.Clock; import java.util.Collection; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; public class HookStageExecutor { @@ -123,6 +127,7 @@ public Future> executeEntrypointStag .withExecutionPlan(planForEntrypointStage(endpoint)) .withInitialPayload(EntrypointPayloadImpl.of(queryParams, headers, body)) .withInvocationContextProvider(invocationContextProvider(endpoint)) + .withModulesExecution(Collections.emptyMap()) .withRejectAllowed(true) .execute(); } @@ -259,7 +264,7 @@ private StageExecutorcreate(hookCatalog, vertx, clock, isConfigToInvokeRequired) + return StageExecutor.create(hookCatalog, vertx, clock) .withStage(stage) .withEntity(entity) .withHookExecutionContext(context); @@ -273,9 +278,30 @@ private StageExecutor modulesExecutionForAccount(Account account) { + final Map accountModulesExecution = Optional.ofNullable(account.getHooks()) + .map(AccountHooksConfiguration::getAdmin) + .map(HooksAdminConfig::getModuleExecution) + .orElse(Collections.emptyMap()); + + final Map resultModulesExecution = new HashMap<>(accountModulesExecution); + + if (isConfigToInvokeRequired) { + Optional.ofNullable(account.getHooks()) + .map(AccountHooksConfiguration::getModules) + .map(Map::keySet) + .stream() + .flatMap(Collection::stream) + .forEach(module -> resultModulesExecution.computeIfAbsent(module, key -> true)); + } + + return DefaultedMap.defaultedMap(resultModulesExecution, !isConfigToInvokeRequired); + } + private static ExecutionPlan parseAndValidateExecutionPlan( String executionPlan, JacksonMapper mapper, @@ -402,8 +428,7 @@ private InvocationContextProvider bidderInvocationConte String bidder) { return (timeout, hookId, moduleContext) -> BidderInvocationContextImpl.of( - auctionInvocationContext(endpoint, timeout, auctionContext, hookId, moduleContext), - bidder); + auctionInvocationContext(endpoint, timeout, auctionContext, hookId, moduleContext), bidder); } private Timeout createTimeout(Long timeout) { diff --git a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java index d99cd14030f..caa1ffc4caa 100644 --- a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java @@ -12,13 +12,13 @@ import java.time.Clock; import java.util.ArrayList; +import java.util.Map; class StageExecutor { private final HookCatalog hookCatalog; private final Vertx vertx; private final Clock clock; - private final boolean isConfigToInvokeRequired; private StageWithHookType> stage; private String entity; @@ -27,21 +27,20 @@ class StageExecutor { private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; + private Map modulesExecution; - private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock, boolean isConfigToInvokeRequired) { + private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) { this.hookCatalog = hookCatalog; this.vertx = vertx; this.clock = clock; - this.isConfigToInvokeRequired = isConfigToInvokeRequired; } public static StageExecutor create( HookCatalog hookCatalog, Vertx vertx, - Clock clock, - boolean isConfigToInvokeRequired) { + Clock clock) { - return new StageExecutor<>(hookCatalog, vertx, clock, isConfigToInvokeRequired); + return new StageExecutor<>(hookCatalog, vertx, clock); } public StageExecutor withStage(StageWithHookType> stage) { @@ -81,6 +80,11 @@ public StageExecutor withRejectAllowed(boolean rejectAllowed) return this; } + public StageExecutor withModulesExecution(Map modulesExecution) { + this.modulesExecution = modulesExecution; + return this; + } + public Future> execute() { Future> stageFuture = Future.succeededFuture(StageResult.of(initialPayload, entity)); @@ -97,7 +101,7 @@ public Future> execute() { } private Future> executeGroup(ExecutionGroup group, PAYLOAD initialPayload) { - return GroupExecutor.create(vertx, clock, isConfigToInvokeRequired) + return GroupExecutor.create(vertx, clock, modulesExecution) .withGroup(group) .withInitialPayload(initialPayload) .withHookProvider( diff --git a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java index 75d3b03b6e5..f45af371385 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java +++ b/src/main/java/org/prebid/server/settings/model/AccountHooksConfiguration.java @@ -14,4 +14,6 @@ public class AccountHooksConfiguration { ExecutionPlan executionPlan; Map modules; + + HooksAdminConfig admin; } diff --git a/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java new file mode 100644 index 00000000000..36b12a401f0 --- /dev/null +++ b/src/main/java/org/prebid/server/settings/model/HooksAdminConfig.java @@ -0,0 +1,16 @@ +package org.prebid.server.settings.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import lombok.Builder; +import lombok.Value; + +import java.util.Map; + +@Builder +@Value +public class HooksAdminConfig { + + @JsonAlias("module-execution") + Map moduleExecution; + +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy index 24f3ab97d77..bab4ec983a3 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/AccountHooksConfiguration.groovy @@ -13,4 +13,5 @@ class AccountHooksConfiguration { @JsonProperty("execution_plan") ExecutionPlan executionPlanSnakeCase PbsModulesConfig modules + AdminConfig admin } diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy new file mode 100644 index 00000000000..755a47bcbaa --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AdminConfig.groovy @@ -0,0 +1,13 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString +import org.prebid.server.functional.model.ModuleName + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AdminConfig { + + Map moduleExecution +} diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy index f1a72a9e266..0f155bf55a1 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/TraceOutcome.groovy @@ -3,13 +3,12 @@ package org.prebid.server.functional.model.response.auction import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.ToString -import org.prebid.server.functional.model.config.Stage @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.LowerCaseStrategy) class TraceOutcome { - Stage entity + String entity Long executionTimeMillis List groups } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy index fa2929665a6..15aab0a3e2f 100644 --- a/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy +++ b/src/test/groovy/org/prebid/server/functional/tests/module/GeneralModuleSpec.groovy @@ -1,40 +1,53 @@ package org.prebid.server.functional.tests.module +import org.prebid.server.functional.model.ModuleName import org.prebid.server.functional.model.config.AccountConfig import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.AdminConfig import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Ortb2BlockingConfig import org.prebid.server.functional.model.config.PbResponseCorrection import org.prebid.server.functional.model.config.PbsModulesConfig +import org.prebid.server.functional.model.config.Stage import org.prebid.server.functional.model.db.Account import org.prebid.server.functional.model.request.auction.RichmediaFilter import org.prebid.server.functional.model.request.auction.TraceLevel import org.prebid.server.functional.model.response.auction.InvocationResult import org.prebid.server.functional.service.PrebidServerService import org.prebid.server.functional.util.PBSUtils +import spock.lang.PendingFeature -import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION +import static org.prebid.server.functional.model.ModuleName.ORTB2_BLOCKING import static org.prebid.server.functional.model.ModuleName.PB_RICHMEDIA_FILTER import static org.prebid.server.functional.model.config.Endpoint.OPENRTB2_AUCTION +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_BIDDER_REQUEST +import static org.prebid.server.functional.model.config.ModuleHookImplementation.ORTB2_BLOCKING_RAW_BIDDER_RESPONSE import static org.prebid.server.functional.model.config.ModuleHookImplementation.PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES -import static org.prebid.server.functional.model.config.ModuleHookImplementation.RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES import static org.prebid.server.functional.model.config.Stage.ALL_PROCESSED_BID_RESPONSES +import static org.prebid.server.functional.model.config.Stage.BIDDER_REQUEST +import static org.prebid.server.functional.model.config.Stage.RAW_BIDDER_RESPONSE import static org.prebid.server.functional.model.request.auction.BidRequest.getDefaultBidRequest import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_ACTION -import static org.prebid.server.functional.model.response.auction.ResponseAction.NO_INVOCATION class GeneralModuleSpec extends ModuleBaseSpec { - private final static String NO_INVOCATION_METRIC = "modules.module.%s.stage.%s.hook.%s.success.no-invocation" private final static String CALL_METRIC = "modules.module.%s.stage.%s.hook.%s.call" + private final static String NOOP_METRIC = "modules.module.%s.stage.%s.hook.%s.success.noop" private final static Map DISABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'false'] private final static Map ENABLED_INVOKE_CONFIG = ['settings.modules.require-config-to-invoke': 'true'] - private final static Map MULTY_MODULE_CONFIG = getRichMediaFilterSettings(PBSUtils.randomString) + getResponseCorrectionConfig() + - ['hooks.host-execution-plan': encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION]]))] - private final static PrebidServerService pbsServiceWithMultipleModule = pbsServiceFactory.getService(MULTY_MODULE_CONFIG + DISABLED_INVOKE_CONFIG) - private final static PrebidServerService pbsServiceWithMultipleModuleWithRequireInvoke = pbsServiceFactory.getService(MULTY_MODULE_CONFIG + ENABLED_INVOKE_CONFIG) + private final static Map> ORTB_STAGES = [(BIDDER_REQUEST) : [ORTB2_BLOCKING], + (RAW_BIDDER_RESPONSE): [ORTB2_BLOCKING]] + private final static Map> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RICHMEDIA_FILTER]] + private final static Map> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES + private final static Map MULTI_MODULE_CONFIG = getRichMediaFilterSettings(PBSUtils.randomString) + + getOrtb2BlockingSettings() + + ['hooks.host-execution-plan': encode(ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES))] + + private final static PrebidServerService pbsServiceWithMultipleModule = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + DISABLED_INVOKE_CONFIG) + private final static PrebidServerService pbsServiceWithMultipleModuleWithRequireInvoke = pbsServiceFactory.getService(MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG) def "PBS should call all modules and traces response when account config is empty and require-config-to-invoke is disabled"() { given: "Default bid request with verbose trace" @@ -55,19 +68,21 @@ class GeneralModuleSpec extends ModuleBaseSpec { then: "PBS response should include trace information about called modules" verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { - it.status == [SUCCESS, SUCCESS] - it.action == [NO_ACTION, NO_ACTION] - it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING, PB_RICHMEDIA_FILTER].code.sort() } - and: "no-invocation metrics shouldn't be updated" + and: "Ortb2blocking module call metrics should be updated" def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 - and: "hook call metrics should be updated" + and: "RB-Richmedia-Filter module call metrics should be updated" assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 - assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 where: modulesConfig << [null, new PbsModulesConfig()] @@ -93,19 +108,21 @@ class GeneralModuleSpec extends ModuleBaseSpec { then: "PBS response should include trace information about called modules" verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { - it.status == [SUCCESS, SUCCESS] - it.action == [NO_ACTION, NO_ACTION] - it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() } - and: "no-invocation metrics shouldn't be updated" + and: "Ortb2blocking module call metrics should be updated" def metrics = pbsServiceWithMultipleModule.sendCollectedMetricsRequest() - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 - and: "hook call metrics should be updated" + and: "RB-Richmedia-Filter module call metrics should be updated" assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 - assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 where: pbRichmediaFilterConfig | pbResponseCorrectionConfig @@ -116,41 +133,47 @@ class GeneralModuleSpec extends ModuleBaseSpec { new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true) } - def "PBS shouldn't call any modules and traces that in response when account config is empty and require-config-to-invoke is enabled"() { - given: "Default bid request with verbose trace" + def "PBS should call all modules and traces response when default-account includes modules config and require-config-to-invoke is enabled"() { + given: "PBS service with module config" + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter(), ortb2Blocking: new Ortb2BlockingConfig()) + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(modules: pbsModulesConfig) + } + + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" def bidRequest = defaultBidRequest.tap { ext.prebid.trace = TraceLevel.VERBOSE } - and: "Save account without modules config" - def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) - def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) - accountDao.save(account) - and: "Flush metrics" - flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + flushMetrics(pbsServiceWithMultipleModules) when: "PBS processes auction request" - def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) - then: "PBS response should include trace information about no-called modules" + then: "PBS response should include trace information about called modules" verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { - it.status == [SUCCESS, SUCCESS] - it.action == [NO_INVOCATION, NO_INVOCATION] - it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() } - and: "no-invocation metrics should be updated" - def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() - assert metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 - assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 - and: "hook call metrics shouldn't be updated" - assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] - assert !metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 - where: - modulesConfig << [null, new PbsModulesConfig()] + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } def "PBS should call all modules and traces response when account includes modules config and require-config-to-invoke is enabled"() { @@ -160,7 +183,7 @@ class GeneralModuleSpec extends ModuleBaseSpec { } and: "Save account with enabled response correction module" - def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, pbResponseCorrection: pbResponseCorrectionConfig) + def pbsModulesConfig = new PbsModulesConfig(pbRichmediaFilter: pbRichmediaFilterConfig, ortb2Blocking: ortb2BlockingConfig) def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: pbsModulesConfig)) def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) accountDao.save(account) @@ -173,27 +196,29 @@ class GeneralModuleSpec extends ModuleBaseSpec { then: "PBS response should include trace information about called modules" verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { - it.status == [SUCCESS, SUCCESS] - it.action == [NO_ACTION, NO_ACTION] - it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, PB_RESPONSE_CORRECTION].code.sort() + it.status == [SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER, ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() } - and: "no-invocation metrics shouldn't be updated" + and: "Ortb2blocking module call metrics should be updated" def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 - and: "hook call metrics should be updated" + and: "RB-Richmedia-Filter module call metrics should be updated" assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 - assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 where: - pbRichmediaFilterConfig | pbResponseCorrectionConfig - new RichmediaFilter() | new PbResponseCorrection() - new RichmediaFilter() | new PbResponseCorrection(enabled: false) - new RichmediaFilter() | new PbResponseCorrection(enabled: true) - new RichmediaFilter(filterMraid: true) | new PbResponseCorrection() - new RichmediaFilter(filterMraid: true) | new PbResponseCorrection(enabled: true) + pbRichmediaFilterConfig | ortb2BlockingConfig + new RichmediaFilter() | new Ortb2BlockingConfig() + new RichmediaFilter() | new Ortb2BlockingConfig(attributes: [:] as Map) + new RichmediaFilter() | new Ortb2BlockingConfig(attributes: [:] as Map) + new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig() + new RichmediaFilter(filterMraid: true) | new Ortb2BlockingConfig(attributes: [:] as Map) } def "PBS should call specified module and traces response when account config includes that module and require-config-to-invoke is enabled"() { @@ -203,7 +228,7 @@ class GeneralModuleSpec extends ModuleBaseSpec { } and: "Save account with enabled response correction module" - def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig(pbResponseCorrection: new PbResponseCorrection()))) + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: new PbsModulesConfig(pbRichmediaFilter: new RichmediaFilter()))) def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) accountDao.save(account) @@ -215,26 +240,234 @@ class GeneralModuleSpec extends ModuleBaseSpec { then: "PBS response should include trace information about called module" def invocationTrace = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List - verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RESPONSE_CORRECTION.code }) { + verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RICHMEDIA_FILTER.code }) { it.status == [SUCCESS] it.action == [NO_ACTION] - it.hookId.moduleCode.sort() == [PB_RESPONSE_CORRECTION].code.sort() + it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER].code.sort() } - and: "PBS response should include trace information about no-called module" - verifyAll(invocationTrace.findAll { it -> it.hookId.moduleCode == PB_RICHMEDIA_FILTER.code }) { - it.status == [SUCCESS] - it.action == [NO_INVOCATION] - it.hookId.moduleCode.sort() == [PB_RICHMEDIA_FILTER].code.sort() + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics should be updated" + assert metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + } + + def "PBS shouldn't call any modules and traces that in response when account config is empty and require-config-to-invoke is enabled"() { + given: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE } - and: "Richmedia module metrics should be updated" + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: modulesConfig)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModuleWithRequireInvoke) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModuleWithRequireInvoke.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" def metrics = pbsServiceWithMultipleModuleWithRequireInvoke.sendCollectedMetricsRequest() - assert metrics[NO_INVOCATION_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] == 1 + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + where: + modulesConfig << [null, new PbsModulesConfig()] + } + + @PendingFeature + def "PBS should call all modules without account config when modules enabled in module-execution host config"() { + given: "PBS service with module-execution config" + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'true'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should call module without account config when default-account module-execution config enabled module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + [("hooks.admin.module-execution.${ORTB2_BLOCKING.code}".toString()): 'false'] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't call any modules without account config when default-account module-execution config not enabling module"() { + given: "PBS service with module-execution and default account configs" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): moduleExecutionStatus])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(modules: null)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response shouldn't include trace information about no-called modules" + assert !response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() + + and: "Ortb2blocking module call metrics shouldn't be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" + assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + moduleExecutionStatus << [false, null] + } + + def "PBS should prioritize specific account module-execution config over default-account module-execution config when both are present"() { + given: "PBS service with default account config" + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): false])) + } + def pbsConfig = MULTI_MODULE_CONFIG + ENABLED_INVOKE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + + and: "Save account without modules config" + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(admin: new AdminConfig(moduleExecution: [(ORTB2_BLOCKING): true]))) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + verifyAll(response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.hookId.moduleCode.sort() == [ORTB2_BLOCKING, ORTB2_BLOCKING].code.sort() + } + + and: "Ortb2blocking module call metrics should be updated" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NOOP_METRIC.formatted(ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "RB-Richmedia-Filter module call metrics shouldn't be updated" assert !metrics[CALL_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] + assert !metrics[NOOP_METRIC.formatted(PB_RICHMEDIA_FILTER.code, ALL_PROCESSED_BID_RESPONSES.metricValue, PB_RICHMEDIA_FILTER_ALL_PROCESSED_RESPONSES.code)] - and: "Response-correction module metrics should be updated" - assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] - assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 1 + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) } } diff --git a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java index 22d8e49f6a2..6bcbe818fec 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -2,6 +2,8 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import io.vertx.core.CompositeFuture; @@ -80,6 +82,7 @@ import org.prebid.server.proto.openrtb.ext.response.BidType; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountHooksConfiguration; +import org.prebid.server.settings.model.HooksAdminConfig; import java.time.Clock; import java.time.ZoneOffset; @@ -1177,7 +1180,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan execPlanOneGroupOneHook("module-beta", "hook-b"))))); final Account account = Account.builder() .id("accountId") - .hooks(AccountHooksConfiguration.of(accountPlan, null)) + .hooks(AccountHooksConfiguration.of(accountPlan, null, null)) .build(); final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); @@ -1233,7 +1236,7 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla HookId.of("module-beta", "hook-a"))))))))); final Account account = Account.builder() .id("accountId") - .hooks(AccountHooksConfiguration.of(accountPlan, null)) + .hooks(AccountHooksConfiguration.of(accountPlan, null, null)) .build(); final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); @@ -1289,7 +1292,7 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla } @Test - public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsRequiredButAbsent(VertxTestContext context) { + public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsNotRequired(VertxTestContext context) { // given givenRawAuctionRequestHook( "module-alpha", @@ -1298,11 +1301,116 @@ public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsRequiredBut payload.bidRequest().toBuilder().at(1).build())))); givenRawAuctionRequestHook( - "module-alpha", + "module-beta", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().test(1).build())))); + + givenRawAuctionRequestHook( + "module-gamma", "hook-b", immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().id("id").build())))); + givenRawAuctionRequestHook( + "module-delta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().tmax(1000L).build())))); + + givenRawAuctionRequestHook( + "module-epsilon", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().site(Site.builder().build()).build())))); + + givenRawAuctionRequestHook( + "module-zeta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().user(User.builder().build()).build())))); + + final StageExecutionPlan stageExecutionPlan = StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-gamma", "hook-b"), + HookId.of("module-delta", "hook-b"))))); + + final String hostExecutionPlan = executionPlan(singletonMap( + Endpoint.openrtb2_auction, + EndpointExecutionPlan.of(singletonMap(Stage.raw_auction_request, stageExecutionPlan)))); + + final HookStageExecutor executor = HookStageExecutor.create( + hostExecutionPlan, + null, + hookCatalog, + timeoutFactory, + vertx, + clock, + jacksonMapper, + false); + + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + + // when + final BidRequest givenBidRequest = BidRequest.builder().build(); + final Future> future = executor.executeRawAuctionRequestStage( + AuctionContext.builder() + .bidRequest(givenBidRequest) + .account(Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of( + null, + Map.of("module-alpha", mapper.createObjectNode(), + "module-beta", mapper.createObjectNode(), + "module-gamma", mapper.createObjectNode()), + HooksAdminConfig.builder() + .moduleExecution(Map.of( + "module-alpha", true, + "module-beta", false)) + .build())) + .build()) + .hookExecutionContext(hookExecutionContext) + .debugContext(DebugContext.empty()) + .build()); + + // then + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .tmax(1000L) + .build())); + + assertThat(hookExecutionContext.getStageOutcomes()) + .hasEntrySatisfying( + Stage.raw_auction_request, + stageOutcomes -> assertThat(stageOutcomes) + .hasSize(1) + .extracting(StageExecutionOutcome::getEntity) + .containsOnly("auction-request")); + + context.completeNow(); + })); + } + + @Test + public void shouldExecuteRawAuctionRequestHooksWhenAccountConfigIsRequired(VertxTestContext context) { + // given + givenRawAuctionRequestHook( + "module-alpha", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().at(1).build())))); + givenRawAuctionRequestHook( "module-beta", "hook-a", @@ -1310,16 +1418,46 @@ public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsRequiredBut payload.bidRequest().toBuilder().test(1).build())))); givenRawAuctionRequestHook( - "module-beta", + "module-gamma", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().id("id").build())))); + + givenRawAuctionRequestHook( + "module-delta", "hook-b", immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( payload.bidRequest().toBuilder().tmax(1000L).build())))); + givenRawAuctionRequestHook( + "module-epsilon", + "hook-a", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().site(Site.builder().build()).build())))); + + givenRawAuctionRequestHook( + "module-zeta", + "hook-b", + immediateHook(InvocationResultUtils.succeeded(payload -> AuctionRequestPayloadImpl.of( + payload.bidRequest().toBuilder().user(User.builder().build()).build())))); + + final StageExecutionPlan stageExecutionPlan = StageExecutionPlan.of(asList( + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-alpha", "hook-a"), + HookId.of("module-beta", "hook-a"), + HookId.of("module-epsilon", "hook-a"))), + ExecutionGroup.of( + 200L, + asList( + HookId.of("module-gamma", "hook-b"), + HookId.of("module-delta", "hook-b"), + HookId.of("module-zeta", "hook-b"))))); + final String hostExecutionPlan = executionPlan(singletonMap( Endpoint.openrtb2_auction, - EndpointExecutionPlan.of(singletonMap( - Stage.raw_auction_request, - execPlanTwoGroupsTwoHooksEach())))); + EndpointExecutionPlan.of(singletonMap(Stage.raw_auction_request, stageExecutionPlan)))); final HookStageExecutor executor = HookStageExecutor.create( hostExecutionPlan, @@ -1338,7 +1476,19 @@ public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsRequiredBut final Future> future = executor.executeRawAuctionRequestStage( AuctionContext.builder() .bidRequest(givenBidRequest) - .account(Account.empty("accountId")) + .account(Account.builder() + .id("accountId") + .hooks(AccountHooksConfiguration.of( + null, + Map.of("module-alpha", mapper.createObjectNode(), + "module-beta", mapper.createObjectNode(), + "module-gamma", mapper.createObjectNode()), + HooksAdminConfig.builder() + .moduleExecution(Map.of( + "module-alpha", true, + "module-beta", false)) + .build())) + .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) .build()); @@ -1346,9 +1496,11 @@ public void shouldNotExecuteRawAuctionRequestHooksWhenAccountConfigIsRequiredBut // then future.onComplete(context.succeeding(result -> { assertThat(result).isNotNull(); - assertThat(result.getPayload()).isNotNull() - .extracting(AuctionRequestPayload::bidRequest) - .isEqualTo(givenBidRequest); + assertThat(result.getPayload()).isNotNull().satisfies(payload -> + assertThat(payload.bidRequest()).isEqualTo(BidRequest.builder() + .at(1) + .id("id") + .build())); assertThat(hookExecutionContext.getStageOutcomes()) .hasEntrySatisfying( @@ -1464,7 +1616,7 @@ public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(V AuctionContext.builder() .bidRequest(BidRequest.builder().build()) .account(Account.builder() - .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration)) + .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration, null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -1738,7 +1890,7 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationCon AuctionContext.builder() .bidRequest(BidRequest.builder().build()) .account(Account.builder() - .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration)) + .hooks(AccountHooksConfiguration.of(null, accountModulesConfiguration, null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2025,7 +2177,7 @@ public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxT .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2180,7 +2332,7 @@ public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(Ve .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2341,7 +2493,7 @@ public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationCont .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2506,7 +2658,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationCo .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty()) @@ -2648,7 +2800,7 @@ public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(Ver .bidRequest(BidRequest.builder().build()) .account(Account.builder() .hooks(AccountHooksConfiguration.of( - null, singletonMap("module-alpha", mapper.createObjectNode()))) + null, singletonMap("module-alpha", mapper.createObjectNode()), null)) .build()) .hookExecutionContext(hookExecutionContext) .debugContext(DebugContext.empty())