From d74e387a39d9724af3fc928c4d6df7cf8fd9da07 Mon Sep 17 00:00:00 2001 From: Dubyk Danylo <45672370+CTMBNara@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:21:18 +0100 Subject: [PATCH] Modules: AB testing (#3576) --- .../adquality/core/AnalyticsMapper.java | 8 +- ...ConfiantAdQualityBidResponsesScanHook.java | 2 +- .../v1/model/analytics/ActivityImpl.java | 19 - .../v1/model/analytics/AppliedToImpl.java | 24 - .../v1/model/analytics/ResultImpl.java | 18 - .../v1/model/analytics/TagsImpl.java | 15 - .../adquality/core/AnalyticsMapperTest.java | 6 +- ...iantAdQualityBidResponsesScanHookTest.java | 6 +- ...FiftyOneDeviceDetectionEntrypointHook.java | 2 +- ...eDeviceDetectionRawAuctionRequestHook.java | 2 +- ...alTimeDataProcessedAuctionRequestHook.java | 18 +- .../data/v1/model/analytics/ActivityImpl.java | 19 - .../v1/model/analytics/AppliedToImpl.java | 24 - .../data/v1/model/analytics/ResultImpl.java | 18 - .../data/v1/model/analytics/TagsImpl.java | 15 - ...meDataProcessedAuctionRequestHookTest.java | 16 +- .../v1/Ortb2BlockingBidderRequestHook.java | 4 +- .../Ortb2BlockingRawBidderResponseHook.java | 12 +- .../v1/model/BidderRequestPayloadImpl.java | 13 - .../v1/model/BidderResponsePayloadImpl.java | 15 - .../Ortb2BlockingBidderRequestHookTest.java | 4 +- ...rtb2BlockingRawBidderResponseHookTest.java | 12 +- ...RequestCorrectionProcessedAuctionHook.java | 21 +- .../v1/model/AuctionRequestPayloadImpl.java | 13 - .../v1/model/InvocationResultImpl.java | 37 - ...orrectionAllProcessedBidResponsesHook.java | 2 +- ...diaFilterAllProcessedBidResponsesHook.java | 10 +- .../v1/model/analytics/ActivityImpl.java | 19 - .../v1/model/analytics/AppliedToImpl.java | 24 - .../filter/v1/model/analytics/ResultImpl.java | 18 - .../filter/v1/model/analytics/TagsImpl.java | 15 - ...ilterAllProcessedBidResponsesHookTest.java | 8 +- .../server/hooks/execution/GroupExecutor.java | 39 +- .../server/hooks/execution/HookCatalog.java | 28 +- .../hooks/execution/HookStageExecutor.java | 183 ++- .../server/hooks/execution/StageExecutor.java | 17 +- .../server/hooks/execution/model/ABTest.java | 29 + .../hooks/execution/model/ExecutionPlan.java | 7 +- .../execution/provider/HookProvider.java | 11 + .../execution/provider/abtest/ABTestHook.java | 148 +++ .../provider/abtest/ABTestHookProvider.java | 87 ++ .../execution/v1}/InvocationResultImpl.java | 2 +- .../execution/v1}/analytics/ActivityImpl.java | 2 +- .../v1}/analytics/AppliedToImpl.java | 4 +- .../execution/v1}/analytics/ResultImpl.java | 2 +- .../execution/v1}/analytics/TagsImpl.java | 2 +- .../server/hooks/v1/InvocationResultImpl.java | 32 - .../functional/model/config/AbTest.groovy | 31 + .../model/config/ExecutionPlan.groovy | 4 + .../model/request/auction/FetchStatus.groovy | 2 +- .../response/auction/InvocationResult.groovy | 1 + .../response/auction/InvocationStatus.groovy | 2 +- .../auction/ModuleActivityName.groovy | 3 +- .../model/response/auction/ModuleValue.groovy | 2 + .../tests/module/AbTestingModuleSpec.groovy | 1157 +++++++++++++++++ .../GreenbidsAnalyticsReporterTest.java | 6 +- .../auction/BidResponseCreatorTest.java | 15 +- .../server/auction/ExchangeServiceTest.java | 8 +- .../hooks/execution/HookCatalogTest.java | 38 +- .../execution/HookStageExecutorTest.java | 205 ++- .../abtest/ABTestHookProviderTest.java | 132 ++ .../provider/abtest/ABTestHookTest.java | 183 +++ .../hooks/v1/InvocationResultUtils.java | 2 + .../hooks/v1/analytics/ActivityImpl.java | 17 - .../hooks/v1/analytics/AppliedToImpl.java | 23 - .../server/hooks/v1/analytics/ResultImpl.java | 16 - .../server/hooks/v1/analytics/TagsImpl.java | 13 - .../hooks/SampleItRawAuctionRequestHook.java | 10 +- 68 files changed, 2220 insertions(+), 682 deletions(-) delete mode 100644 extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java delete mode 100644 extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java delete mode 100644 extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java delete mode 100644 extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java delete mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java delete mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java delete mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java delete mode 100644 extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java delete mode 100644 extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java delete mode 100644 extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java delete mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java delete mode 100644 extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java delete mode 100644 extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java delete mode 100644 extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java delete mode 100644 extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java delete mode 100644 extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java create mode 100644 src/main/java/org/prebid/server/hooks/execution/model/ABTest.java create mode 100644 src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java create mode 100644 src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java create mode 100644 src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java rename {extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model => src/main/java/org/prebid/server/hooks/execution/v1}/InvocationResultImpl.java (90%) rename {extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model => src/main/java/org/prebid/server/hooks/execution/v1}/analytics/ActivityImpl.java (82%) rename {extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model => src/main/java/org/prebid/server/hooks/execution/v1}/analytics/AppliedToImpl.java (83%) rename {extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model => src/main/java/org/prebid/server/hooks/execution/v1}/analytics/ResultImpl.java (84%) rename {extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model => src/main/java/org/prebid/server/hooks/execution/v1}/analytics/TagsImpl.java (81%) delete mode 100644 src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java create mode 100644 src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy create mode 100644 src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy create mode 100644 src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java create mode 100644 src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java delete mode 100644 src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java delete mode 100644 src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java delete mode 100644 src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java delete mode 100644 src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java index 57eac3d3620..0a7e9c7c2ea 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapper.java @@ -3,10 +3,10 @@ import com.iab.openrtb.response.Bid; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.analytics.AppliedTo; import org.prebid.server.hooks.v1.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java index 2db501f1852..8a65e74db63 100644 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java +++ b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHook.java @@ -20,7 +20,7 @@ import org.prebid.server.hooks.modules.com.confiant.adquality.model.GroupByIssues; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index 4453cb34e12..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 34beae0b73b..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java deleted file mode 100644 index 439552f562f..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java b/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java deleted file mode 100644 index 1c01790d6b8..00000000000 --- a/extra/modules/confiant-ad-quality/src/main/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java index 9ec01a7cfed..f3ea0d4764e 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/core/AnalyticsMapperTest.java @@ -2,10 +2,10 @@ import org.junit.jupiter.api.Test; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.v1.analytics.Tags; import java.util.List; diff --git a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java index d4b39a8214e..926865781d3 100644 --- a/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java +++ b/extra/modules/confiant-ad-quality/src/test/java/org/prebid/server/hooks/modules/com/confiant/adquality/v1/ConfiantAdQualityBidResponsesScanHookTest.java @@ -16,15 +16,15 @@ import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsMapper; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanResult; import org.prebid.server.hooks.modules.com.confiant.adquality.core.BidsScanner; import org.prebid.server.hooks.modules.com.confiant.adquality.core.RedisParser; import org.prebid.server.hooks.modules.com.confiant.adquality.util.AdQualityModuleTestUtils; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.com.confiant.adquality.v1.model.analytics.ResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java index 506cf66078e..6a652ccf109 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionEntrypointHook.java @@ -5,7 +5,7 @@ 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.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.entrypoint.EntrypointHook; import org.prebid.server.hooks.v1.entrypoint.EntrypointPayload; diff --git a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java index 8b0d5cdc8d4..5c4b268cf68 100644 --- a/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java +++ b/extra/modules/fiftyone-devicedetection/src/main/java/org/prebid/server/hooks/modules/fiftyone/devicedetection/v1/hooks/FiftyOneDeviceDetectionRawAuctionRequestHook.java @@ -15,7 +15,7 @@ import org.prebid.server.hooks.modules.fiftyone.devicedetection.v1.model.ModuleContext; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java index 0f20fde7041..3b677a78a18 100644 --- a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java +++ b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHook.java @@ -10,21 +10,21 @@ import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; -import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; -import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.Partner; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.data.ThrottlingMessage; import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.GreenbidsInvocationResult; -import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.InvocationResultImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index 421541e59da..00000000000 --- a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 68ad76ccdf3..00000000000 --- a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java deleted file mode 100644 index d234a59eb31..00000000000 --- a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java b/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java deleted file mode 100644 index 899e797dab2..00000000000 --- a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java index fed56cf009e..e1bde277c06 100644 --- a/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java +++ b/extra/modules/greenbids-real-time-data/src/test/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/GreenbidsRealTimeDataProcessedAuctionRequestHookTest.java @@ -22,24 +22,24 @@ import org.prebid.server.analytics.reporter.greenbids.model.ExplorationResult; import org.prebid.server.analytics.reporter.greenbids.model.Ortb2ImpExtResult; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.greenbids.real.time.data.config.DatabaseReaderFactory; -import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; -import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; -import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.FilterService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInferenceDataService; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ModelCache; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunner; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerFactory; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.OnnxModelRunnerWithThresholds; import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThresholdCache; +import org.prebid.server.hooks.modules.greenbids.real.time.data.core.ThrottlingThresholdsFactory; +import org.prebid.server.hooks.modules.greenbids.real.time.data.model.filter.ThrottlingThresholds; import org.prebid.server.hooks.modules.greenbids.real.time.data.model.result.AnalyticsResult; -import org.prebid.server.hooks.modules.greenbids.real.time.data.core.GreenbidsInvocationService; import org.prebid.server.hooks.modules.greenbids.real.time.data.util.TestBidRequestProvider; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java index bb5e05d4fb2..6ef69c93140 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHook.java @@ -5,15 +5,15 @@ import org.prebid.server.auction.BidderAliases; import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.BlockedAttributesResolver; import org.prebid.server.hooks.modules.ortb2.blocking.core.RequestUpdater; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationAction; 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.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java index c90ac94840a..720823f4513 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java +++ b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHook.java @@ -6,20 +6,20 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.versionconverter.OrtbVersion; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.BidsBlocker; import org.prebid.server.hooks.modules.ortb2.blocking.core.ResponseUpdater; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.AnalyticsResult; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedBids; import org.prebid.server.hooks.modules.ortb2.blocking.core.model.ExecutionResult; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; 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.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java deleted file mode 100644 index bd394217b21..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderRequestPayloadImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import com.iab.openrtb.request.BidRequest; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class BidderRequestPayloadImpl implements BidderRequestPayload { - - BidRequest bidRequest; -} diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java b/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java deleted file mode 100644 index 72d678c89a5..00000000000 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/BidderResponsePayloadImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.bidder.model.BidderBid; -import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class BidderResponsePayloadImpl implements BidderResponsePayload { - - List bids; -} diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java index 4c3852bc630..5c1b7303831 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingBidderRequestHookTest.java @@ -18,6 +18,8 @@ import org.prebid.server.auction.versionconverter.OrtbVersion; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.bidder.BidderInfo; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.ArrayOverride; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; @@ -27,10 +29,8 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationAction; 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.PayloadUpdate; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; diff --git a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java index 356bd7b2125..351bfbb9d33 100644 --- a/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java +++ b/extra/modules/ortb2-blocking/src/test/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/Ortb2BlockingRawBidderResponseHookTest.java @@ -13,6 +13,12 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.bidder.model.BidderBid; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attribute; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.AttributeActionOverrides; import org.prebid.server.hooks.modules.ortb2.blocking.core.config.Attributes; @@ -22,14 +28,8 @@ import org.prebid.server.hooks.modules.ortb2.blocking.core.model.BlockedAttributes; import org.prebid.server.hooks.modules.ortb2.blocking.model.ModuleContext; import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderInvocationContextImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.BidderResponsePayloadImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; 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.PayloadUpdate; import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java index 50502e844db..b9142c93d26 100644 --- a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java +++ b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/RequestCorrectionProcessedAuctionHook.java @@ -6,11 +6,11 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; import org.prebid.server.exception.PreBidException; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.pb.request.correction.core.RequestCorrectionProvider; import org.prebid.server.hooks.modules.pb.request.correction.core.config.model.Config; import org.prebid.server.hooks.modules.pb.request.correction.core.correction.Correction; -import org.prebid.server.hooks.modules.pb.request.correction.v1.model.AuctionRequestPayloadImpl; -import org.prebid.server.hooks.modules.pb.request.correction.v1.model.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -55,12 +55,13 @@ public Future> call(AuctionRequestPayloa return noAction(); } - final InvocationResult invocationResult = InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.update) - .payloadUpdate(initialPayload -> - AuctionRequestPayloadImpl.of(applyCorrections(initialPayload.bidRequest(), corrections))) - .build(); + final InvocationResult invocationResult = + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(initialPayload -> AuctionRequestPayloadImpl.of( + applyCorrections(initialPayload.bidRequest(), corrections))) + .build(); return Future.succeededFuture(invocationResult); } @@ -82,7 +83,7 @@ private static BidRequest applyCorrections(BidRequest bidRequest, List> failure(String message) { - return Future.succeededFuture(InvocationResultImpl.builder() + return Future.succeededFuture(InvocationResultImpl.builder() .status(InvocationStatus.failure) .message(message) .action(InvocationAction.no_action) @@ -90,7 +91,7 @@ private Future> failure(String message) } private static Future> noAction() { - return Future.succeededFuture(InvocationResultImpl.builder() + return Future.succeededFuture(InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.no_action) .build()); diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java deleted file mode 100644 index ca8bb6aa52d..00000000000 --- a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/AuctionRequestPayloadImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.modules.pb.request.correction.v1.model; - -import com.iab.openrtb.request.BidRequest; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class AuctionRequestPayloadImpl implements AuctionRequestPayload { - - BidRequest bidRequest; -} diff --git a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java b/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java deleted file mode 100644 index 96f90d14a29..00000000000 --- a/extra/modules/pb-request-correction/src/main/java/org/prebid/server/hooks/modules/pb/request/correction/v1/model/InvocationResultImpl.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.hooks.modules.pb.request.correction.v1.model; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.InvocationAction; -import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.PayloadUpdate; -import org.prebid.server.hooks.v1.analytics.Tags; -import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; -} diff --git a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java index 4d7fb81c366..9f9e8e75659 100644 --- a/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-response-correction/src/main/java/org/prebid/server/hooks/modules/pb/response/correction/v1/ResponseCorrectionAllProcessedBidResponsesHook.java @@ -13,7 +13,7 @@ import org.prebid.server.hooks.modules.pb.response.correction.core.correction.Correction; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; -import org.prebid.server.hooks.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.bidder.AllProcessedBidResponsesHook; diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java index e0416c6d30d..c63baeda58c 100644 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java +++ b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHook.java @@ -6,19 +6,19 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.auction.model.BidderResponse; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; 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.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java deleted file mode 100644 index bb9e887ca02..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ActivityImpl.java +++ /dev/null @@ -1,19 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java deleted file mode 100644 index 24f793287b5..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/AppliedToImpl.java +++ /dev/null @@ -1,24 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; - -import java.util.List; - -@Accessors(fluent = true) -@Value -@Builder -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java deleted file mode 100644 index e15359f5c14..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/ResultImpl.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.AppliedTo; -import org.prebid.server.hooks.v1.analytics.Result; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java b/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java deleted file mode 100644 index b996bcb4355..00000000000 --- a/extra/modules/pb-richmedia-filter/src/main/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/model/analytics/TagsImpl.java +++ /dev/null @@ -1,15 +0,0 @@ -package org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java index 47d5ab27253..2a87faec776 100644 --- a/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java +++ b/extra/modules/pb-richmedia-filter/src/test/java/org/prebid/server/hooks/modules/pb/richmedia/filter/v1/PbRichmediaFilterAllProcessedBidResponsesHookTest.java @@ -11,16 +11,16 @@ import org.prebid.server.auction.model.BidRejectionTracker; import org.prebid.server.auction.model.BidderResponse; import org.prebid.server.bidder.model.BidderSeatBid; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.BidResponsesMraidFilter; import org.prebid.server.hooks.modules.pb.richmedia.filter.core.ModuleConfigResolver; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.AnalyticsResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.MraidFilterResult; import org.prebid.server.hooks.modules.pb.richmedia.filter.model.PbRichMediaFilterProperties; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ActivityImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.AppliedToImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.ResultImpl; -import org.prebid.server.hooks.modules.pb.richmedia.filter.v1.model.analytics.TagsImpl; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; 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 6ec0cc63095..e8d2266d630 100644 --- a/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/GroupExecutor.java @@ -7,36 +7,30 @@ import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.HookExecutionContext; import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.provider.HookProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.InvocationResult; -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; class GroupExecutor { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(GroupExecutor.class)); - private final Vertx vertx; private final Clock clock; private final Map modulesExecution; private ExecutionGroup group; private PAYLOAD initialPayload; - private Function> hookProvider; + private HookProvider hookProvider; private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; private GroupExecutor(Vertx vertx, Clock clock, Map modulesExecution) { - this.vertx = vertx; this.clock = clock; this.modulesExecution = modulesExecution; @@ -60,7 +54,7 @@ public GroupExecutor withInitialPayload(PAYLOAD initialPayload return this; } - public GroupExecutor withHookProvider(Function> hookProvider) { + public GroupExecutor withHookProvider(HookProvider hookProvider) { this.hookProvider = hookProvider; return this; } @@ -91,11 +85,11 @@ public Future> execute() { continue; } - final Hook hook = hookProvider.apply(hookId); + final Future> hookFuture = hook(hookId); final long startTime = clock.millis(); - final Future> invocationResult = - executeHook(hook, group.getTimeout(), initialGroupResult, hookId); + final Future> invocationResult = hookFuture + .compose(hook -> executeHook(hook, group.getTimeout(), initialGroupResult, hookId)); groupFuture = groupFuture.compose(groupResult -> applyInvocationResult(invocationResult, hookId, startTime, groupResult)); @@ -104,17 +98,18 @@ public Future> execute() { return groupFuture.recover(GroupExecutor::restoreResultFromRejection); } - private Future> executeHook( - Hook hook, - Long timeout, - GroupResult groupResult, - HookId hookId) { - - if (hook == null) { - conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d); - - return Future.failedFuture(new FailedException("Hook implementation does not exist or disabled")); + private Future> hook(HookId hookId) { + try { + return Future.succeededFuture(hookProvider.apply(hookId)); + } catch (Exception e) { + return Future.failedFuture(new FailedException(e.getMessage())); } + } + + private Future> executeHook(Hook hook, + Long timeout, + GroupResult groupResult, + HookId hookId) { final CONTEXT invocationContext = invocationContextProvider.apply(timeout, hookId, moduleContextFor(hookId)); return executeWithTimeout(() -> hook.call(groupResult.payload(), invocationContext), timeout); diff --git a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java index 754e3925b11..f58b7f136c7 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookCatalog.java @@ -1,38 +1,46 @@ package org.prebid.server.hooks.execution; +import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.StageWithHookType; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.Module; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.util.Collection; import java.util.Objects; -/** - * Provides simple access to all {@link Hook}s registered in application. - */ public class HookCatalog { + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(HookCatalog.class)); + private final Collection modules; public HookCatalog(Collection modules) { this.modules = Objects.requireNonNull(modules); } - public > HOOK hookById( - String moduleCode, - String hookImplCode, - StageWithHookType stage) { + public > HOOK hookById(HookId hookId, + StageWithHookType stage) { final Class clazz = stage.hookType(); return modules.stream() - .filter(module -> Objects.equals(module.code(), moduleCode)) + .filter(module -> Objects.equals(module.code(), hookId.getModuleCode())) .map(Module::hooks) .flatMap(Collection::stream) - .filter(hook -> Objects.equals(hook.code(), hookImplCode)) + .filter(hook -> Objects.equals(hook.code(), hookId.getHookImplCode())) .filter(clazz::isInstance) .map(clazz::cast) .findFirst() - .orElse(null); + .orElseThrow(() -> { + logAbsentHook(hookId); + return new IllegalArgumentException("Hook implementation does not exist or disabled"); + }); + } + + private static void logAbsentHook(HookId hookId) { + conditionalLogger.error("Hook implementation %s does not exist or disabled".formatted(hookId), 0.01d); } } 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 534205225b3..048b42bb0aa 100644 --- a/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/HookStageExecutor.java @@ -1,10 +1,13 @@ package org.prebid.server.hooks.execution; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; import io.vertx.core.Vertx; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.ListUtils; import org.apache.commons.collections4.map.DefaultedMap; import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; @@ -14,6 +17,7 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ABTest; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; @@ -23,6 +27,8 @@ import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.execution.provider.abtest.ABTestHookProvider; import org.prebid.server.hooks.execution.v1.InvocationContextImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionInvocationContextImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -47,8 +53,8 @@ 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 org.prebid.server.settings.model.HooksAdminConfig; import java.time.Clock; import java.util.Collection; @@ -58,6 +64,7 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.stream.Stream; public class HookStageExecutor { @@ -74,6 +81,7 @@ public class HookStageExecutor { private final TimeoutFactory timeoutFactory; private final Vertx vertx; private final Clock clock; + private final ObjectMapper mapper; private final boolean isConfigToInvokeRequired; private HookStageExecutor(ExecutionPlan hostExecutionPlan, @@ -82,6 +90,7 @@ private HookStageExecutor(ExecutionPlan hostExecutionPlan, TimeoutFactory timeoutFactory, Vertx vertx, Clock clock, + ObjectMapper mapper, boolean isConfigToInvokeRequired) { this.hostExecutionPlan = hostExecutionPlan; @@ -90,6 +99,7 @@ private HookStageExecutor(ExecutionPlan hostExecutionPlan, this.timeoutFactory = timeoutFactory; this.vertx = vertx; this.clock = clock; + this.mapper = mapper; this.isConfigToInvokeRequired = isConfigToInvokeRequired; } @@ -102,19 +112,62 @@ public static HookStageExecutor create(String hostExecutionPlan, JacksonMapper mapper, boolean isConfigToInvokeRequired) { + Objects.requireNonNull(hookCatalog); + Objects.requireNonNull(mapper); + return new HookStageExecutor( - parseAndValidateExecutionPlan( - hostExecutionPlan, - Objects.requireNonNull(mapper), - Objects.requireNonNull(hookCatalog)), + parseAndValidateExecutionPlan(hostExecutionPlan, mapper, hookCatalog), parseAndValidateExecutionPlan(defaultAccountExecutionPlan, mapper, hookCatalog), hookCatalog, Objects.requireNonNull(timeoutFactory), Objects.requireNonNull(vertx), Objects.requireNonNull(clock), + mapper.mapper(), isConfigToInvokeRequired); } + private static ExecutionPlan parseAndValidateExecutionPlan(String executionPlan, + JacksonMapper mapper, + HookCatalog hookCatalog) { + + return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog); + } + + private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { + if (StringUtils.isBlank(executionPlan)) { + return ExecutionPlan.empty(); + } + + try { + return mapper.decodeValue(executionPlan, ExecutionPlan.class); + } catch (DecodeException e) { + throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); + } + } + + private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) { + plan.getEndpoints().values().stream() + .map(EndpointExecutionPlan::getStages) + .map(Map::entrySet) + .flatMap(Collection::stream) + .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream() + .map(ExecutionGroup::getHookSequence) + .flatMap(Collection::stream) + .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); + + return plan; + } + + private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { + try { + hookCatalog.hookById(hookId, StageWithHookType.forStage(stage)); + } catch (Throwable e) { + throw new IllegalArgumentException( + "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s" + .formatted(stage, hookId)); + } + } + public Future> executeEntrypointStage( CaseInsensitiveMultiMap queryParams, CaseInsensitiveMultiMap headers, @@ -125,6 +178,7 @@ public Future> executeEntrypointStag return stageExecutor(StageWithHookType.ENTRYPOINT, ENTITY_HTTP_REQUEST, context) .withExecutionPlan(planForEntrypointStage(endpoint)) + .withHookProvider(hookProviderForEntrypointStage(context)) .withInitialPayload(EntrypointPayloadImpl.of(queryParams, headers, body)) .withInvocationContextProvider(invocationContextProvider(endpoint)) .withModulesExecution(Collections.emptyMap()) @@ -264,7 +318,7 @@ private StageExecutorcreate(hookCatalog, vertx, clock) + return StageExecutor.create(vertx, clock) .withStage(stage) .withEntity(entity) .withHookExecutionContext(context); @@ -279,7 +333,8 @@ private StageExecutor modulesExecutionForAccount(Account account) { @@ -302,52 +357,6 @@ private Map modulesExecutionForAccount(Account account) { return DefaultedMap.defaultedMap(resultModulesExecution, !isConfigToInvokeRequired); } - private static ExecutionPlan parseAndValidateExecutionPlan( - String executionPlan, - JacksonMapper mapper, - HookCatalog hookCatalog) { - - return validateExecutionPlan(parseExecutionPlan(executionPlan, mapper), hookCatalog); - } - - private static ExecutionPlan validateExecutionPlan(ExecutionPlan plan, HookCatalog hookCatalog) { - plan.getEndpoints().values().stream() - .map(EndpointExecutionPlan::getStages) - .map(Map::entrySet) - .flatMap(Collection::stream) - .forEach(stageToPlan -> stageToPlan.getValue().getGroups().stream() - .map(ExecutionGroup::getHookSequence) - .flatMap(Collection::stream) - .forEach(hookId -> validateHookId(stageToPlan.getKey(), hookId, hookCatalog))); - - return plan; - } - - private static void validateHookId(Stage stage, HookId hookId, HookCatalog hookCatalog) { - final Hook hook = hookCatalog.hookById( - hookId.getModuleCode(), - hookId.getHookImplCode(), - StageWithHookType.forStage(stage)); - - if (hook == null) { - throw new IllegalArgumentException( - "Hooks execution plan contains unknown or disabled hook: stage=%s, hookId=%s" - .formatted(stage, hookId)); - } - } - - private static ExecutionPlan parseExecutionPlan(String executionPlan, JacksonMapper mapper) { - if (StringUtils.isBlank(executionPlan)) { - return ExecutionPlan.empty(); - } - - try { - return mapper.decodeValue(executionPlan, ExecutionPlan.class); - } catch (DecodeException e) { - throw new IllegalArgumentException("Hooks execution plan could not be parsed", e); - } - } - private StageExecutionPlan planForEntrypointStage(Endpoint endpoint) { return effectiveStagePlanFrom(ExecutionPlan.empty(), endpoint, Stage.entrypoint); } @@ -392,6 +401,34 @@ private ExecutionPlan effectiveExecutionPlanFor(Account account) { return accountExecutionPlan != null ? accountExecutionPlan : defaultAccountExecutionPlan; } + private HookProvider hookProviderForEntrypointStage( + HookExecutionContext context) { + + return new ABTestHookProvider<>( + defaultHookProvider(StageWithHookType.ENTRYPOINT), + abTestsForEntrypointStage(), + context, + mapper); + } + + private HookProvider hookProvider( + StageWithHookType> stage, + Account account, + HookExecutionContext context) { + + return new ABTestHookProvider<>( + defaultHookProvider(stage), + abTests(account), + context, + mapper); + } + + private HookProvider defaultHookProvider( + StageWithHookType> stage) { + + return hookId -> hookCatalog.hookById(hookId, stage); + } + private InvocationContextProvider invocationContextProvider(Endpoint endpoint) { return (timeout, hookId, moduleContext) -> invocationContext(endpoint, timeout); } @@ -428,7 +465,8 @@ 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) { @@ -442,4 +480,41 @@ private static ObjectNode accountConfigFor(Account account, HookId hookId) { return modulesConfiguration != null ? modulesConfiguration.get(hookId.getModuleCode()) : null; } + + protected List abTestsForEntrypointStage() { + return ListUtils.emptyIfNull(hostExecutionPlan.getAbTests()).stream() + .filter(HookStageExecutor::isABTestEnabled) + .toList(); + } + + private static boolean isABTestEnabled(ABTest abTest) { + return abTest != null && abTest.isEnabled(); + } + + protected List abTests(Account account) { + return abTestsFromAccount(account) + .or(() -> abTestsFromHostConfig(account.getId())) + .orElse(Collections.emptyList()); + } + + private Optional> abTestsFromAccount(Account account) { + return Optional.of(effectiveExecutionPlanFor(account)) + .map(ExecutionPlan::getAbTests) + .map(abTests -> abTests.stream() + .filter(HookStageExecutor::isABTestEnabled) + .toList()); + } + + private Optional> abTestsFromHostConfig(String accountId) { + return Optional.ofNullable(hostExecutionPlan.getAbTests()) + .map(abTests -> abTests.stream() + .filter(HookStageExecutor::isABTestEnabled) + .filter(abTest -> isABTestApplicable(abTest, accountId)) + .toList()); + } + + private static boolean isABTestApplicable(ABTest abTest, String account) { + final Set accounts = abTest.getAccounts(); + return CollectionUtils.isEmpty(accounts) || accounts.contains(account); + } } 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 caa1ffc4caa..8dfa03e9a7f 100644 --- a/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java +++ b/src/main/java/org/prebid/server/hooks/execution/StageExecutor.java @@ -7,6 +7,7 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.provider.HookProvider; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; @@ -16,31 +17,29 @@ class StageExecutor { - private final HookCatalog hookCatalog; private final Vertx vertx; private final Clock clock; private StageWithHookType> stage; private String entity; private StageExecutionPlan executionPlan; + private HookProvider hookProvider; private PAYLOAD initialPayload; private InvocationContextProvider invocationContextProvider; private HookExecutionContext hookExecutionContext; private boolean rejectAllowed; private Map modulesExecution; - private StageExecutor(HookCatalog hookCatalog, Vertx vertx, Clock clock) { - this.hookCatalog = hookCatalog; + private StageExecutor(Vertx vertx, Clock clock) { this.vertx = vertx; this.clock = clock; } public static StageExecutor create( - HookCatalog hookCatalog, Vertx vertx, Clock clock) { - return new StageExecutor<>(hookCatalog, vertx, clock); + return new StageExecutor<>(vertx, clock); } public StageExecutor withStage(StageWithHookType> stage) { @@ -58,6 +57,11 @@ public StageExecutor withExecutionPlan(StageExecutionPlan exec return this; } + public StageExecutor withHookProvider(HookProvider hookProvider) { + this.hookProvider = hookProvider; + return this; + } + public StageExecutor withInitialPayload(PAYLOAD initialPayload) { this.initialPayload = initialPayload; return this; @@ -104,8 +108,7 @@ private Future> executeGroup(ExecutionGroup group, PAYLOAD return GroupExecutor.create(vertx, clock, modulesExecution) .withGroup(group) .withInitialPayload(initialPayload) - .withHookProvider( - hookId -> hookCatalog.hookById(hookId.getModuleCode(), hookId.getHookImplCode(), stage)) + .withHookProvider(hookProvider) .withInvocationContextProvider(invocationContextProvider) .withHookExecutionContext(hookExecutionContext) .withRejectAllowed(rejectAllowed) diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java new file mode 100644 index 00000000000..67d64190101 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/model/ABTest.java @@ -0,0 +1,29 @@ +package org.prebid.server.hooks.execution.model; + +import com.fasterxml.jackson.annotation.JsonAlias; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; +import lombok.Value; + +import java.util.Set; + +@Builder +@Value +public class ABTest { + + boolean enabled; + + @JsonProperty("module-code") + @JsonAlias("module_code") + String moduleCode; + + Set accounts; + + @JsonProperty("percent-active") + @JsonAlias("percent_active") + Integer percentActive; + + @JsonProperty("log-analytics-tag") + @JsonAlias("log_analytics_tag") + Boolean logAnalyticsTag; +} diff --git a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java index 5d0af8b8e23..8d865c26a90 100644 --- a/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java +++ b/src/main/java/org/prebid/server/hooks/execution/model/ExecutionPlan.java @@ -1,15 +1,20 @@ package org.prebid.server.hooks.execution.model; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import org.prebid.server.model.Endpoint; import java.util.Collections; +import java.util.List; import java.util.Map; @Value(staticConstructor = "of") public class ExecutionPlan { - private static final ExecutionPlan EMPTY = of(Collections.emptyMap()); + private static final ExecutionPlan EMPTY = of(null, Collections.emptyMap()); + + @JsonProperty("abtests") + List abTests; Map endpoints; diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java new file mode 100644 index 00000000000..83297c26396 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/HookProvider.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.execution.provider; + +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.function.Function; + +public interface HookProvider + extends Function> { +} diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java new file mode 100644 index 00000000000..e7b82803f9a --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHook.java @@ -0,0 +1,148 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +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.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.util.ListUtil; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +public class ABTestHook implements Hook { + + private static final String ANALYTICS_ACTIVITY_NAME = "core-module-abtests"; + + private final String moduleName; + private final Hook hook; + private final boolean shouldInvokeHook; + private final boolean logABTestAnalyticsTag; + private final ObjectMapper mapper; + + public ABTestHook(String moduleName, + Hook hook, + boolean shouldInvokeHook, + boolean logABTestAnalyticsTag, + ObjectMapper mapper) { + + this.moduleName = Objects.requireNonNull(moduleName); + this.hook = Objects.requireNonNull(hook); + this.shouldInvokeHook = shouldInvokeHook; + this.logABTestAnalyticsTag = logABTestAnalyticsTag; + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public String code() { + return hook.code(); + } + + @Override + public Future> call(PAYLOAD payload, CONTEXT invocationContext) { + if (!shouldInvokeHook) { + return skippedResult(); + } + + final Future> invocationResultFuture = hook.call(payload, invocationContext); + return logABTestAnalyticsTag + ? invocationResultFuture.map(this::enrichWithABTestAnalyticsTag) + : invocationResultFuture; + } + + private Future> skippedResult() { + return Future.succeededFuture(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_invocation) + .analyticsTags(logABTestAnalyticsTag ? tags("skipped") : null) + .build()); + } + + private Tags tags(String status) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ANALYTICS_ACTIVITY_NAME, + "success", + Collections.singletonList(ResultImpl.of(status, analyticsValues(), null))))); + } + + private ObjectNode analyticsValues() { + final ObjectNode values = mapper.createObjectNode(); + values.put("module", moduleName); + return values; + } + + private InvocationResult enrichWithABTestAnalyticsTag(InvocationResult invocationResult) { + return new InvocationResultWithAdditionalTags<>(invocationResult, tags("run")); + } + + private record InvocationResultWithAdditionalTags(InvocationResult invocationResult, + Tags additionalTags) + implements InvocationResult { + + @Override + public InvocationStatus status() { + return invocationResult.status(); + } + + @Override + public String message() { + return invocationResult.message(); + } + + @Override + public InvocationAction action() { + return invocationResult.action(); + } + + @Override + public PayloadUpdate payloadUpdate() { + return invocationResult.payloadUpdate(); + } + + @Override + public List errors() { + return invocationResult.errors(); + } + + @Override + public List warnings() { + return invocationResult.warnings(); + } + + @Override + public List debugMessages() { + return invocationResult.debugMessages(); + } + + @Override + public Object moduleContext() { + return invocationResult.moduleContext(); + } + + @Override + public Tags analyticsTags() { + return new TagsUnion(invocationResult.analyticsTags(), additionalTags); + } + } + + private record TagsUnion(Tags left, Tags right) implements Tags { + + @Override + public List activities() { + return left != null + ? ListUtil.union(left.activities(), right.activities()) + : right.activities(); + } + } +} diff --git a/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java new file mode 100644 index 00000000000..6a833ab2833 --- /dev/null +++ b/src/main/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProvider.java @@ -0,0 +1,87 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.prebid.server.hooks.execution.model.ABTest; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.ThreadLocalRandom; + +public class ABTestHookProvider implements HookProvider { + + private final HookProvider innerHookProvider; + private final List abTests; + private final HookExecutionContext context; + private final ObjectMapper mapper; + + public ABTestHookProvider(HookProvider innerHookProvider, + List abTests, + HookExecutionContext context, + ObjectMapper mapper) { + + this.innerHookProvider = Objects.requireNonNull(innerHookProvider); + this.abTests = Objects.requireNonNull(abTests); + this.context = Objects.requireNonNull(context); + this.mapper = Objects.requireNonNull(mapper); + } + + @Override + public Hook apply(HookId hookId) { + final Hook hook = innerHookProvider.apply(hookId); + + final String moduleCode = hookId.getModuleCode(); + final ABTest abTest = searchForABTest(moduleCode); + if (abTest == null) { + return hook; + } + + return new ABTestHook<>( + moduleCode, + hook, + shouldInvokeHook(moduleCode, abTest), + BooleanUtils.isNotFalse(abTest.getLogAnalyticsTag()), + mapper); + } + + private ABTest searchForABTest(String moduleCode) { + return abTests.stream() + .filter(abTest -> moduleCode.equals(abTest.getModuleCode())) + .findFirst() + .orElse(null); + } + + protected boolean shouldInvokeHook(String moduleCode, ABTest abTest) { + final HookExecutionOutcome hookExecutionOutcome = searchForPreviousExecution(moduleCode); + if (hookExecutionOutcome != null) { + return hookExecutionOutcome.getAction() != ExecutionAction.no_invocation; + } + + final int percent = ObjectUtils.defaultIfNull(abTest.getPercentActive(), 100); + return ThreadLocalRandom.current().nextInt(100) < percent; + } + + private HookExecutionOutcome searchForPreviousExecution(String moduleCode) { + return context.getStageOutcomes().values().stream() + .filter(Objects::nonNull) + .flatMap(Collection::stream) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hookExecutionOutcome -> hookExecutionOutcome.getHookId().getModuleCode().equals(moduleCode)) + .findFirst() + .orElse(null); + } +} diff --git a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java similarity index 90% rename from extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java index 4efcb2bd5c2..761aef951ec 100644 --- a/extra/modules/greenbids-real-time-data/src/main/java/org/prebid/server/hooks/modules/greenbids/real/time/data/v1/model/InvocationResultImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/InvocationResultImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.greenbids.real.time.data.v1.model; +package org.prebid.server.hooks.execution.v1; import lombok.Builder; import lombok.Value; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java similarity index 82% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java index 484489a5e6f..4c9747e16bc 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ActivityImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ActivityImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import lombok.Value; import lombok.experimental.Accessors; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java similarity index 83% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java index 2971cc40d6e..884603b4717 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/AppliedToImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/AppliedToImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import lombok.Builder; import lombok.Value; @@ -8,8 +8,8 @@ import java.util.List; @Accessors(fluent = true) -@Value @Builder +@Value public class AppliedToImpl implements AppliedTo { List impIds; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java similarity index 84% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java index 5405799e25f..c16397e894c 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/ResultImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/ResultImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; diff --git a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java similarity index 81% rename from extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java rename to src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java index 9f0432b9e2f..f068f28dcef 100644 --- a/extra/modules/ortb2-blocking/src/main/java/org/prebid/server/hooks/modules/ortb2/blocking/v1/model/analytics/TagsImpl.java +++ b/src/main/java/org/prebid/server/hooks/execution/v1/analytics/TagsImpl.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.ortb2.blocking.v1.model.analytics; +package org.prebid.server.hooks.execution.v1.analytics; import lombok.Value; import lombok.experimental.Accessors; diff --git a/src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java b/src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java deleted file mode 100644 index abfe5cf8fb2..00000000000 --- a/src/main/java/org/prebid/server/hooks/v1/InvocationResultImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.prebid.server.hooks.v1; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class InvocationResultImpl implements InvocationResult { - - InvocationStatus status; - - String message; - - InvocationAction action; - - PayloadUpdate payloadUpdate; - - List errors; - - List warnings; - - List debugMessages; - - Object moduleContext; - - Tags analyticsTags; -} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy new file mode 100644 index 00000000000..baa19a80db4 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/model/config/AbTest.groovy @@ -0,0 +1,31 @@ +package org.prebid.server.functional.model.config + +import com.fasterxml.jackson.annotation.JsonProperty +import com.fasterxml.jackson.databind.PropertyNamingStrategies +import com.fasterxml.jackson.databind.annotation.JsonNaming +import groovy.transform.ToString + +@ToString(includeNames = true, ignoreNulls = true) +@JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) +class AbTest { + + Boolean enabled + String moduleCode + @JsonProperty("module_code") + String moduleCodeSnakeCase + Set accounts + Integer percentActive + @JsonProperty("percent_active") + Integer percentActiveSnakeCase + Boolean logAnalyticsTag + @JsonProperty("log_analytics_tag") + Boolean logAnalyticsTagSnakeCase + + static AbTest getDefault(String moduleCode, List accounts = null) { + new AbTest(enabled: true, + moduleCode: moduleCode, + accounts: accounts, + percentActive: 0, + logAnalyticsTag: true) + } +} diff --git a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy index 9e5cfc3ac93..653f8c8cbea 100644 --- a/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/config/ExecutionPlan.groovy @@ -1,11 +1,15 @@ 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.LowerCaseStrategy) class ExecutionPlan { + List abTests Map endpoints static ExecutionPlan getSingleEndpointExecutionPlan(Endpoint endpoint, ModuleName moduleName, List stage) { diff --git a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy index c669d61f5a0..6eec49cf39d 100644 --- a/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/request/auction/FetchStatus.groovy @@ -6,7 +6,7 @@ import groovy.transform.ToString @ToString enum FetchStatus { - NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK + NONE, SUCCESS, TIMEOUT, INPROGRESS, ERROR, SUCCESS_ALLOW, SUCCESS_BLOCK, SKIPPED, RUN @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy index 3f1594380f4..c5c1a828f98 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationResult.groovy @@ -14,4 +14,5 @@ class InvocationResult { ResponseAction action HookId hookId AnalyticsPrebidTag analyticsTags + String message } diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy index 257b6287fcf..77c7ffd5ef8 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/InvocationStatus.groovy @@ -6,7 +6,7 @@ import groovy.transform.ToString @ToString enum InvocationStatus { - SUCCESS, FAILURE + SUCCESS, FAILURE, INVOCATION_FAILURE @JsonValue String getValue() { diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy index 3942b170875..8711bd395c6 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleActivityName.groovy @@ -5,7 +5,8 @@ import com.fasterxml.jackson.annotation.JsonValue enum ModuleActivityName { ORTB2_BLOCKING('enforce-blocking'), - REJECT_RICHMEDIA('reject-richmedia') + REJECT_RICHMEDIA('reject-richmedia'), + AB_TESTING('core-module-abtests') @JsonValue final String value diff --git a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy index e76e8fb3f54..9a1e9d1b440 100644 --- a/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy +++ b/src/test/groovy/org/prebid/server/functional/model/response/auction/ModuleValue.groovy @@ -4,11 +4,13 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategies import com.fasterxml.jackson.databind.annotation.JsonNaming import groovy.transform.EqualsAndHashCode import groovy.transform.ToString +import org.prebid.server.functional.model.ModuleName @ToString(includeNames = true, ignoreNulls = true) @JsonNaming(PropertyNamingStrategies.KebabCaseStrategy) @EqualsAndHashCode class ModuleValue { + ModuleName module String richmediaFormat } diff --git a/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy new file mode 100644 index 00000000000..9ca353e0088 --- /dev/null +++ b/src/test/groovy/org/prebid/server/functional/tests/module/AbTestingModuleSpec.groovy @@ -0,0 +1,1157 @@ +package org.prebid.server.functional.tests.module + +import org.prebid.server.functional.model.ModuleName +import org.prebid.server.functional.model.config.AbTest +import org.prebid.server.functional.model.config.AccountConfig +import org.prebid.server.functional.model.config.AccountHooksConfiguration +import org.prebid.server.functional.model.config.ExecutionPlan +import org.prebid.server.functional.model.config.Stage +import org.prebid.server.functional.model.db.Account +import org.prebid.server.functional.model.request.auction.BidRequest +import org.prebid.server.functional.model.request.auction.FetchStatus +import org.prebid.server.functional.model.request.auction.TraceLevel +import org.prebid.server.functional.model.response.auction.AnalyticResult +import org.prebid.server.functional.model.response.auction.InvocationResult +import org.prebid.server.functional.service.PrebidServerService +import org.prebid.server.functional.util.PBSUtils + +import static org.prebid.server.functional.model.ModuleName.PB_RESPONSE_CORRECTION +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.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.INVOCATION_FAILURE +import static org.prebid.server.functional.model.response.auction.InvocationStatus.SUCCESS +import static org.prebid.server.functional.model.response.auction.ModuleActivityName.AB_TESTING +import static org.prebid.server.functional.model.response.auction.ModuleActivityName.ORTB2_BLOCKING +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 AbTestingModuleSpec 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 EXECUTION_ERROR_METRIC = "modules.module.%s.stage.%s.hook.%s.execution-error" + private final static Integer MIN_PERCENT_AB = 0 + private final static Integer MAX_PERCENT_AB = 100 + private final static String INVALID_HOOK_MESSAGE = "Hook implementation does not exist or disabled" + + private final static Map> ORTB_STAGES = [(BIDDER_REQUEST) : [ModuleName.ORTB2_BLOCKING], + (RAW_BIDDER_RESPONSE): [ModuleName.ORTB2_BLOCKING]] + private final static Map> RESPONSE_STAGES = [(ALL_PROCESSED_BID_RESPONSES): [PB_RESPONSE_CORRECTION]] + private final static Map> MODULES_STAGES = ORTB_STAGES + RESPONSE_STAGES + + private final static Map MULTI_MODULE_CONFIG = getResponseCorrectionConfig() + getOrtb2BlockingSettings() + + ['hooks.host-execution-plan': null] + + private final static PrebidServerService ortbModulePbsService = pbsServiceFactory.getService(getOrtb2BlockingSettings()) + private final static PrebidServerService pbsServiceWithMultipleModules = pbsServiceFactory.getService(MULTI_MODULE_CONFIG) + + def "PBS shouldn't apply a/b test config when config of ab test is disabled"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def abTest = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + enabled = false + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + it.abTests = [abTest] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + } + + def "PBS shouldn't apply valid a/b test config when module is disabled"() { + given: "PBS service with disabled module config" + def pbsConfig = getOrtb2BlockingSettings(false) + def prebidServerService = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(prebidServerService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = prebidServerService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [INVOCATION_FAILURE, INVOCATION_FAILURE] + it.action == [null, null] + it.analyticsTags == [null, null] + it.message == [INVALID_HOOK_MESSAGE, INVALID_HOOK_MESSAGE] + } + + and: "Metric for specified module should be with error call" + def metrics = prebidServerService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[EXECUTION_ERROR_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't apply a/b test config when module name is not matched"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(moduleName)] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + moduleName << [ModuleName.ORTB2_BLOCKING.code.toUpperCase(), PBSUtils.randomString] + } + + def "PBS should apply a/b test config for each module when multiple config are presents and set to allow modules"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for allowed to run ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + + and: "Metric for allowed to run response-correction module should be updated based on ab test config" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + } + + def "PBS should apply a/b test config for each module when multiple config are presents and set to skip modules"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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: "Metric for skipped response-correction module should be updated based on ab test config" + assert !metrics[CALL_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)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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 + } + + def "PBS should apply a/b test config for each module when multiple config are presents with different percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code).tap { + it.percentActive = MAX_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.RUN].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for allowed to run response-correction module should be updated based on ab test config" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + } + + def "PBS should ignore accounts property for a/b test config when ab test config specialize for specific account"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + } + + def "PBS should apply a/b test config and run module when config is on max percentage or default value"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb module and run module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + percentActive | percentActiveSnakeCase + MAX_PERCENT_AB | null + null | MAX_PERCENT_AB + null | null + } + + def "PBS should apply a/b test config and skip module when config is on min percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb module and skip this module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + percentActive | percentActiveSnakeCase + MIN_PERCENT_AB | null + null | MIN_PERCENT_AB + } + + def "PBS shouldn't apply a/b test config without warnings and errors when percent config is out of lover range"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "No error or warning should be emitted" + assert !response.ext.errors + assert !response.ext.warnings + + and: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + percentActive | percentActiveSnakeCase + PBSUtils.randomNegativeNumber | null + null | PBSUtils.randomNegativeNumber + } + + def "PBS should apply a/b test config and run module without warnings and errors when percent config is out of appear range"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + it.percentActive = percentActive + it.percentActiveSnakeCase = percentActiveSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "No error or warning should be emitted" + assert !response.ext.errors + assert !response.ext.warnings + + and: "PBS should apply ab test config for ortb module and run it" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.activities.name.flatten().sort() == [ORTB2_BLOCKING, AB_TESTING, AB_TESTING].value.sort() + it.analyticsTags.activities.status.flatten().sort() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS].sort() + it.analyticsTags.activities.results.status.flatten().sort() == [FetchStatus.SUCCESS_ALLOW, FetchStatus.RUN, FetchStatus.RUN].value.sort() + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + percentActive | percentActiveSnakeCase + PBSUtils.getRandomNumber(MAX_PERCENT_AB) | null + null | PBSUtils.getRandomNumber(MAX_PERCENT_AB) + } + + def "PBS should include analytics tags when a/b test config when logAnalyticsTag is enabled or empty"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module without analytics tags" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + true | null + null | true + null | null + } + + def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is applied by percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + } + + and: "Shouldn't include any analytics tags" + assert !invocationResults?.analyticsTags?.any() + + and: "Metric for specified module should be updated based on ab test config" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + false | null + null | false + } + + def "PBS shouldn't include analytics tags when a/b test config when logAnalyticsTag is disabled and is non-applied by percentage"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MAX_PERCENT_AB + it.logAnalyticsTag = logAnalyticsTag + it.logAnalyticsTagSnakeCase = logAnalyticsTagSnakeCase + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + } + + and: "Shouldn't include any analytics tags" + assert (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name != AB_TESTING.value } + + and: "Metric for specified module should be as default call" + def metrics = ortbModulePbsService.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + where: + logAnalyticsTag | logAnalyticsTagSnakeCase + false | null + null | false + } + + def "PBS shouldn't apply analytics tags for all module stages when module contain multiple stages"() { + given: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(ortbModulePbsService) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, ORTB_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = PBSUtils.getRandomNumber(MIN_PERCENT_AB, MAX_PERCENT_AB) + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = ortbModulePbsService.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for all stages of specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + verifyAll(invocationResults) { + it.status.every { status -> status == it.status.first() } + it.action.every { action -> action == it.action.first() } + } + + and: "All resonances have same analytics" + def abTestingInvocationResults = (invocationResults.analyticsTags.activities.flatten() as List).findAll { it.name == AB_TESTING.value } + verifyAll(abTestingInvocationResults) { + it.status.flatten().every { status -> status == it.status.flatten().first() } + it.results.status.flatten().every { status -> status == it.results.status.flatten().first() } + it.results.values.module.flatten().every { module -> module == it.results.values.module.flatten().first() } + } + } + + def "PBS should apply a/b test config from host config when accounts is not specified when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, accouns).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + + where: + accouns << [null, []] + } + + def "PBS should apply a/b test config from host config for specific accounts and only specified module when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def accountId = PBSUtils.randomNumber + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber, accountId]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + setAccountId(accountId as String) + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should apply a/b test config from host config for specific account and general config when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def accountId = PBSUtils.randomNumber + def ortb2AbTestConfig = AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, []).tap { + it.percentActive = MIN_PERCENT_AB + } + def richMediaAbTestConfig = AbTest.getDefault(PB_RESPONSE_CORRECTION.code, [accountId]).tap { + it.percentActive = MIN_PERCENT_AB + } + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [ortb2AbTestConfig, richMediaAbTestConfig] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + setAccountId(accountId as String) + } + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for ortb2blocking module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should apply ab test config for response-correction module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [PB_RESPONSE_CORRECTION] + } + + and: "Metric for skipped ortb2blocking module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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: "Metric for skipped response-correction module should be updated based on ab test config" + assert !metrics[CALL_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)] + assert metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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 + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS shouldn't apply a/b test config from host config for non specified accounts when account config and default account doesn't include a/b test config"() { + given: "PBS service with specific ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code, [PBSUtils.randomNumber]).tap { + percentActive = MIN_PERCENT_AB + }] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(executionPlan)] + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.activities.name.flatten() == [ORTB2_BLOCKING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SUCCESS_ALLOW].value + it.analyticsTags.activities.results.values.module.flatten().every { it == null } + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be as default call" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[CALL_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] + assert !metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should prioritise a/b test config from default account and only specified module when host and default account contains a/b test configs"() { + given: "PBS service with specific ab test config" + def accountExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + }] + } + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan) + } + + def hostExecutionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)] + } + def pbsConfig = MULTI_MODULE_CONFIG + ['hooks.host-execution-plan': encode(hostExecutionPlan)] + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS should apply ab test config for specified module and call it based on all execution plans" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS, SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION, NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING, AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other modules and call them based on all execution plans" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_ACTION, NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 2 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 2 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2 + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_ALL_PROCESSED_RESPONSES.code)] == 2 + + assert !metrics[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + def "PBS should prioritise a/b test config from account over default account and only specified module when specific account and default account contains a/b test configs"() { + given: "PBS service with specific ab test config" + def accountExecutionPlan = new ExecutionPlan(abTests: [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code)]) + def defaultAccountConfigSettings = AccountConfig.defaultAccountConfig.tap { + hooks = new AccountHooksConfiguration(executionPlan: accountExecutionPlan) + } + + def pbsConfig = MULTI_MODULE_CONFIG + ["settings.default-account-config": encode(defaultAccountConfigSettings)] + + def pbsServiceWithMultipleModules = pbsServiceFactory.getService(pbsConfig) + + and: "Default bid request with verbose trace" + def bidRequest = getBidRequestWithTrace() + + and: "Flush metrics" + flushMetrics(pbsServiceWithMultipleModules) + + and: "Save account with ab test config" + def executionPlan = ExecutionPlan.getSingleEndpointExecutionPlan(OPENRTB2_AUCTION, MODULES_STAGES).tap { + abTests = [AbTest.getDefault(ModuleName.ORTB2_BLOCKING.code).tap { + percentActive = MIN_PERCENT_AB + }] + } + def accountConfig = new AccountConfig(hooks: new AccountHooksConfiguration(executionPlan: executionPlan)) + def account = new Account(uuid: bidRequest.getAccountId(), config: accountConfig) + accountDao.save(account) + + when: "PBS processes auction request" + def response = pbsServiceWithMultipleModules.sendAuctionRequest(bidRequest) + + then: "PBS response should include trace information about called modules" + def invocationResults = response?.ext?.prebid?.modules?.trace?.stages?.outcomes?.groups?.invocationResults?.flatten() as List + + and: "PBS should apply ab test config for specified module" + def ortbBlockingInvocationResults = filterInvocationResultsByModule(invocationResults, ModuleName.ORTB2_BLOCKING) + verifyAll(ortbBlockingInvocationResults) { + it.status == [SUCCESS, SUCCESS] + it.action == [NO_INVOCATION, NO_INVOCATION] + it.analyticsTags.activities.name.flatten() == [AB_TESTING, AB_TESTING].value + it.analyticsTags.activities.status.flatten() == [FetchStatus.SUCCESS, FetchStatus.SUCCESS] + it.analyticsTags.activities.results.status.flatten() == [FetchStatus.SKIPPED, FetchStatus.SKIPPED].value + it.analyticsTags.activities.results.values.module.flatten() == [ModuleName.ORTB2_BLOCKING, ModuleName.ORTB2_BLOCKING] + } + + and: "PBS should not apply ab test config for other module" + def responseCorrectionInvocationResults = filterInvocationResultsByModule(invocationResults, PB_RESPONSE_CORRECTION) + verifyAll(responseCorrectionInvocationResults) { + it.status == [SUCCESS] + it.action == [NO_ACTION] + + it.analyticsTags.every { it == null } + } + + and: "Metric for specified module should be updated based on ab test config" + def metrics = pbsServiceWithMultipleModules.sendCollectedMetricsRequest() + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, BIDDER_REQUEST.metricValue, ORTB2_BLOCKING_BIDDER_REQUEST.code)] == 1 + assert metrics[NO_INVOCATION_METRIC.formatted(ModuleName.ORTB2_BLOCKING.code, RAW_BIDDER_RESPONSE.metricValue, ORTB2_BLOCKING_RAW_BIDDER_RESPONSE.code)] == 1 + + and: "Metric for non specified module should be as default call" + assert metrics[CALL_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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[NO_INVOCATION_METRIC.formatted(PB_RESPONSE_CORRECTION.code, ALL_PROCESSED_BID_RESPONSES.metricValue, RESPONSE_CORRECTION_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)] + + cleanup: "Stop and remove pbs container" + pbsServiceFactory.removeContainer(pbsConfig) + } + + private static List filterInvocationResultsByModule(List invocationResults, ModuleName moduleName) { + invocationResults.findAll { it.hookId.moduleCode == moduleName.code } + } + + private static BidRequest getBidRequestWithTrace() { + defaultBidRequest.tap { + ext.prebid.trace = TraceLevel.VERBOSE + } + } +} diff --git a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java index 9167b1148ec..ab16f5a1b40 100644 --- a/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java +++ b/src/test/java/org/prebid/server/analytics/reporter/greenbids/GreenbidsAnalyticsReporterTest.java @@ -45,9 +45,9 @@ import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionOutcome; -import org.prebid.server.hooks.v1.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.json.EncodeException; import org.prebid.server.json.JacksonMapper; import org.prebid.server.model.HttpRequestContext; diff --git a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java index 2f34799817c..daf9e2d4ff6 100644 --- a/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/BidResponseCreatorTest.java @@ -20,8 +20,6 @@ import com.iab.openrtb.response.Response; import com.iab.openrtb.response.SeatBid; import io.vertx.core.Future; -import lombok.Value; -import lombok.experimental.Accessors; import org.apache.commons.collections4.MapUtils; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; @@ -67,7 +65,7 @@ import org.prebid.server.hooks.execution.HookStageExecutor; import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; -import org.prebid.server.hooks.v1.bidder.BidderResponsePayload; +import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; import org.prebid.server.identity.IdGenerator; import org.prebid.server.identity.IdGeneratorType; import org.prebid.server.proto.openrtb.ext.ExtIncludeBrandCategory; @@ -3830,8 +3828,8 @@ public void createShouldSendCacheRequestWithExpectedTtlAndSetTtlFromBid() { final AuctionContext auctionContext = givenAuctionContext( givenBidRequest(builder -> builder.ext(ExtRequest.of(ExtRequestPrebid.builder() - .events(mapper.createObjectNode()) - .build())), imp), + .events(mapper.createObjectNode()) + .build())), imp), builder -> builder.account(Account.builder() .id("accountId") .auction(AccountAuctionConfig.builder() @@ -5064,11 +5062,4 @@ private static ObjectNode extWithTargeting(String targetBidderCode, Map List mutableList(T... values) { return Arrays.stream(values).collect(Collectors.toCollection(ArrayList::new)); } - - @Accessors(fluent = true) - @Value(staticConstructor = "of") - private static class BidderResponsePayloadImpl implements BidderResponsePayload { - - List bids; - } } diff --git a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java index c50377f0667..8e538acafac 100644 --- a/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java +++ b/src/test/java/org/prebid/server/auction/ExchangeServiceTest.java @@ -84,13 +84,13 @@ import org.prebid.server.hooks.execution.model.HookStageExecutionResult; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.BidderResponsePayloadImpl; -import org.prebid.server.hooks.v1.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.AppliedToImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.log.CriteriaLogManager; import org.prebid.server.log.HttpInteractionLogger; import org.prebid.server.metric.MetricName; diff --git a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java index 108605efe58..330e779a304 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookCatalogTest.java @@ -5,6 +5,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.StageWithHookType; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; @@ -19,6 +20,7 @@ import static java.util.Collections.singleton; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; @@ -41,23 +43,17 @@ public void setUp() { } @Test - public void hookByIdShouldTolerateUnknownModule() { - // when - final EntrypointHook foundHook = hookCatalog.hookById( - "unknown-module", null, StageWithHookType.ENTRYPOINT); - - // then - assertThat(foundHook).isNull(); + public void hookByIdShouldThrowExceptionOnUnknownModule() { + // when and then + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> + hookCatalog.hookById(HookId.of("unknown-module", null), StageWithHookType.ENTRYPOINT)); } @Test - public void hookByIdShouldTolerateUnknownHook() { - // when - final EntrypointHook foundHook = hookCatalog.hookById( - "sample-module", "unknown-hook", StageWithHookType.ENTRYPOINT); - - // then - assertThat(foundHook).isNull(); + public void hookByIdShouldThrowExceptionOnUnknownHook() { + // when and then + assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> + hookCatalog.hookById(HookId.of("sample-module", "unknown-hook"), StageWithHookType.ENTRYPOINT)); } @Test @@ -67,7 +63,7 @@ public void hookByIdShouldReturnEntrypointHook() { // when final EntrypointHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.ENTRYPOINT); + HookId.of("sample-module", "sample-hook"), StageWithHookType.ENTRYPOINT); // then assertThat(foundHook).isNotNull() @@ -82,7 +78,7 @@ public void hookByIdShouldReturnRawAuctionRequestHook() { // when final RawAuctionRequestHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.RAW_AUCTION_REQUEST); + HookId.of("sample-module", "sample-hook"), StageWithHookType.RAW_AUCTION_REQUEST); // then assertThat(foundHook).isNotNull() @@ -97,7 +93,7 @@ public void hookByIdShouldReturnProcessedAuctionRequestHook() { // when final ProcessedAuctionRequestHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.PROCESSED_AUCTION_REQUEST); + HookId.of("sample-module", "sample-hook"), StageWithHookType.PROCESSED_AUCTION_REQUEST); // then assertThat(foundHook).isNotNull() @@ -112,7 +108,7 @@ public void hookByIdShouldReturnBidderRequestHook() { // when final BidderRequestHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.BIDDER_REQUEST); + HookId.of("sample-module", "sample-hook"), StageWithHookType.BIDDER_REQUEST); // then assertThat(foundHook).isNotNull() @@ -127,7 +123,7 @@ public void hookByIdShouldReturnRawBidderResponseHook() { // when final RawBidderResponseHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.RAW_BIDDER_RESPONSE); + HookId.of("sample-module", "sample-hook"), StageWithHookType.RAW_BIDDER_RESPONSE); // then assertThat(foundHook).isNotNull() @@ -142,7 +138,7 @@ public void hookByIdShouldReturnProcessedBidderResponseHook() { // when final ProcessedBidderResponseHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.PROCESSED_BIDDER_RESPONSE); + HookId.of("sample-module", "sample-hook"), StageWithHookType.PROCESSED_BIDDER_RESPONSE); // then assertThat(foundHook).isNotNull() @@ -157,7 +153,7 @@ public void hookByIdShouldReturnAuctionResponseHook() { // when final AuctionResponseHook foundHook = hookCatalog.hookById( - "sample-module", "sample-hook", StageWithHookType.AUCTION_RESPONSE); + HookId.of("sample-module", "sample-hook"), StageWithHookType.AUCTION_RESPONSE); // then assertThat(foundHook).isNotNull() 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 6bcbe818fec..7d4f46adf16 100644 --- a/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java +++ b/src/test/java/org/prebid/server/hooks/execution/HookStageExecutorTest.java @@ -6,7 +6,6 @@ import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; -import io.vertx.core.CompositeFuture; import io.vertx.core.Future; import io.vertx.core.Promise; import io.vertx.core.Vertx; @@ -21,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.VertxTest; @@ -31,6 +31,7 @@ import org.prebid.server.bidder.model.BidderBid; import org.prebid.server.bidder.model.BidderSeatBid; import org.prebid.server.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ABTest; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionAction; import org.prebid.server.hooks.execution.model.ExecutionGroup; @@ -45,6 +46,11 @@ import org.prebid.server.hooks.execution.model.StageExecutionOutcome; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.model.StageWithHookType; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.execution.v1.bidder.AllProcessedBidResponsesPayloadImpl; @@ -54,13 +60,8 @@ 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.InvocationResultUtils; import org.prebid.server.hooks.v1.InvocationStatus; -import org.prebid.server.hooks.v1.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.AppliedToImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; @@ -96,13 +97,13 @@ import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; +import static java.util.Collections.singleton; import static java.util.Collections.singletonList; import static java.util.Collections.singletonMap; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; import static org.mockito.Mock.Strictness.LENIENT; @@ -153,8 +154,8 @@ public void creationShouldFailWhenHostExecutionPlanHasUnknownHook() { HookId.of("module-alpha", "hook-a"), HookId.of("module-beta", "hook-a"))))))))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) - .willReturn(null); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willThrow(new IllegalArgumentException("Exception.")); givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction())); @@ -176,8 +177,8 @@ public void creationShouldFailWhenDefaultAccountExecutionPlanHasUnknownHook() { HookId.of("module-alpha", "hook-a"), HookId.of("module-beta", "hook-a"))))))))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) - .willReturn(null); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) + .willThrow(new IllegalArgumentException("Exception.")); givenEntrypointHook("module-beta", "hook-a", immediateHook(InvocationResultUtils.noAction())); @@ -1049,13 +1050,13 @@ public void shouldExecuteEntrypointHooksAndPassInvocationContext(VertxTestContex // given final EntrypointHookImpl hookImpl = spy( EntrypointHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.ENTRYPOINT))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1112,7 +1113,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert // given final RawAuctionRequestHookImpl hookImpl = spy( RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction()))); - given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(any(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final String hostPlan = executionPlan(singletonMap( @@ -1144,9 +1145,9 @@ public void shouldExecuteRawAuctionRequestHooksWhenNoExecutionPlanInAccount(Vert verify(hookImpl, times(2)).call(any(), any()); verify(hookCatalog, times(2)) - .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); verify(hookCatalog, times(2)) - .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); context.completeNow(); })); @@ -1157,7 +1158,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan // given final RawAuctionRequestHookImpl hookImpl = spy( RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.noAction()))); - given(hookCatalog.hookById(anyString(), anyString(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(any(), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final String hostPlan = executionPlan(singletonMap( @@ -1173,7 +1174,7 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan final HookStageExecutor executor = createExecutor(hostPlan, defaultAccountPlan); final BidRequest bidRequest = BidRequest.builder().build(); - final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap( + final ExecutionPlan accountPlan = ExecutionPlan.of(emptyList(), singletonMap( Endpoint.openrtb2_auction, EndpointExecutionPlan.of(singletonMap( Stage.raw_auction_request, @@ -1200,11 +1201,11 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan verify(hookImpl, times(2)).call(any(), any()); verify(hookCatalog, times(2)) - .hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); verify(hookCatalog) - .hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); verify(hookCatalog) - .hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); + .hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST)); context.completeNow(); })); @@ -1213,8 +1214,8 @@ public void shouldExecuteRawAuctionRequestHooksWhenAccountOverridesExecutionPlan @Test public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPlan(VertxTestContext context) { // given - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) - .willReturn(null); + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + .willThrow(new IllegalArgumentException("Hook implementation does not exist or disabled")); givenRawAuctionRequestHook( "module-beta", @@ -1224,7 +1225,7 @@ public void shouldExecuteRawAuctionRequestHooksToleratingUnknownHookInAccountPla final HookStageExecutor executor = createExecutor(null, null); - final ExecutionPlan accountPlan = ExecutionPlan.of(singletonMap( + final ExecutionPlan accountPlan = ExecutionPlan.of(emptyList(), singletonMap( Endpoint.openrtb2_auction, EndpointExecutionPlan.of(singletonMap( Stage.raw_auction_request, @@ -1587,13 +1588,13 @@ public void shouldExecuteRawAuctionRequestHooksAndPassAuctionInvocationContext(V // given final RawAuctionRequestHookImpl hookImpl = spy( RawAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1677,17 +1678,17 @@ public void shouldExecuteRawAuctionRequestHooksAndPassModuleContextBetweenHooks( .build())); return promise.future(); })); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-c"), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1860,13 +1861,13 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassAuctionInvocationCon // given final ProcessedAuctionRequestHookImpl hookImpl = spy( ProcessedAuctionRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -1951,17 +1952,17 @@ public void shouldExecuteProcessedAuctionRequestHooksAndPassModuleContextBetween .build())); return promise.future(); })); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-a"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-b"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); - given(hookCatalog.hookById(eq("module-beta"), eq("hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook("module-beta", "hook-c"), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2156,7 +2157,7 @@ public void shouldExecuteBidderRequestHooksAndPassBidderInvocationContext(VertxT // given final BidderRequestHookImpl hookImpl = spy( BidderRequestHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.BIDDER_REQUEST))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.BIDDER_REQUEST))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2293,7 +2294,7 @@ public void shouldExecuteRawBidderResponseHooksHappyPath(VertxTestContext contex checkpoint1.flag(); })); - CompositeFuture.join(future1, future2).onComplete(context.succeeding(result -> { + Future.join(future1, future2).onComplete(context.succeeding(result -> { assertThat(hookExecutionContext.getStageOutcomes()) .hasEntrySatisfying( Stage.raw_bidder_response, @@ -2311,7 +2312,7 @@ public void shouldExecuteRawBidderResponseHooksAndPassBidderInvocationContext(Ve // given final RawBidderResponseHookImpl hookImpl = spy( RawBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2469,7 +2470,7 @@ public void shouldExecuteProcessedBidderResponseHooksAndPassBidderInvocationCont // given final ProcessedBidderResponseHookImpl hookImpl = spy( ProcessedBidderResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2634,7 +2635,7 @@ public void shouldExecuteAllProcessedBidResponsesHooksAndPassAuctionInvocationCo // given final AllProcessedBidResponsesHookImpl hookImpl = spy( AllProcessedBidResponsesHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2782,7 +2783,7 @@ public void shouldExecuteAuctionResponseHooksAndPassAuctionInvocationContext(Ver // given final AuctionResponseHookImpl hookImpl = spy( AuctionResponseHookImpl.of(immediateHook(InvocationResultUtils.succeeded(identity())))); - given(hookCatalog.hookById(eq("module-alpha"), eq("hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) + given(hookCatalog.hookById(eqHook("module-alpha", "hook-a"), eq(StageWithHookType.AUCTION_RESPONSE))) .willReturn(hookImpl); final HookStageExecutor executor = createExecutor( @@ -2878,8 +2879,86 @@ public void shouldExecuteAuctionResponseHooksAndIgnoreRejection(VertxTestContext })); } + @Test + public void abTestsForEntrypointStageShouldReturnEnabledTests() { + // given + final HookStageExecutor executor = createExecutor(executionPlan(asList( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("2")).build(), + ABTest.builder().enabled(true).build()))); + + // when + final List abTests = executor.abTestsForEntrypointStage(); + + // then + assertThat(abTests) + .hasSize(2) + .extracting(ABTest::isEnabled) + .containsOnly(true); + } + + @Test + public void abTestsShouldReturnEnabledTestsFromAccount() { + // given + final HookStageExecutor executor = createExecutor(executionPlan(asList( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("2")).build(), + ABTest.builder().enabled(true).build()))); + + final Account account = Account.builder() + .id("1") + .hooks(AccountHooksConfiguration.of( + ExecutionPlan.of( + asList( + ABTest.builder().enabled(true).accounts(singleton("3")).build(), + ABTest.builder().enabled(false).accounts(singleton("4")).build(), + ABTest.builder().enabled(true).build()), + emptyMap()), + emptyMap(), + null)) + .build(); + + // when + final List abTests = executor.abTests(account); + + // then + assertThat(abTests).containsExactly( + ABTest.builder().enabled(true).accounts(singleton("3")).build(), + ABTest.builder().enabled(true).build()); + } + + @Test + public void abTestsShouldReturnEnabledTestsFromHost() { + // given + final HookStageExecutor executor = createExecutor( + executionPlan(asList( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("1")).build(), + ABTest.builder().enabled(false).accounts(singleton("2")).build(), + ABTest.builder().enabled(true).build())), + jacksonMapper.encodeToString(ExecutionPlan.empty())); + + final Account account = Account.builder() + .id("1") + .build(); + + // when + final List abTests = executor.abTests(account); + + // then + assertThat(abTests).containsExactly( + ABTest.builder().enabled(true).accounts(singleton("1")).build(), + ABTest.builder().enabled(true).build()); + } + private String executionPlan(Map endpoints) { - return jacksonMapper.encodeToString(ExecutionPlan.of(endpoints)); + return jacksonMapper.encodeToString(ExecutionPlan.of(null, endpoints)); + } + + private String executionPlan(List abTests) { + return jacksonMapper.encodeToString(ExecutionPlan.of(abTests, emptyMap())); } private static StageExecutionPlan execPlanTwoGroupsTwoHooksEach() { @@ -2908,7 +2987,7 @@ private void givenEntrypointHook( String hookImplCode, BiFunction>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ENTRYPOINT))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.ENTRYPOINT))) .willReturn(EntrypointHookImpl.of(delegate)); } @@ -2920,7 +2999,7 @@ private void givenRawAuctionRequestHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.RAW_AUCTION_REQUEST))) .willReturn(RawAuctionRequestHookImpl.of(delegate)); } @@ -2932,7 +3011,7 @@ private void givenProcessedAuctionRequestHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.PROCESSED_AUCTION_REQUEST))) .willReturn(ProcessedAuctionRequestHookImpl.of(delegate)); } @@ -2944,7 +3023,7 @@ private void givenBidderRequestHook( BidderInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.BIDDER_REQUEST))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.BIDDER_REQUEST))) .willReturn(BidderRequestHookImpl.of(delegate)); } @@ -2956,7 +3035,7 @@ private void givenRawBidderResponseHook( BidderInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.RAW_BIDDER_RESPONSE))) .willReturn(RawBidderResponseHookImpl.of(delegate)); } @@ -2968,7 +3047,7 @@ private void givenProcessedBidderResponseHook( BidderInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.PROCESSED_BIDDER_RESPONSE))) .willReturn(ProcessedBidderResponseHookImpl.of(delegate)); } @@ -2980,7 +3059,7 @@ private void givenAllProcessedBidderResponsesHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.ALL_PROCESSED_BID_RESPONSES))) .willReturn(AllProcessedBidResponsesHookImpl.of(delegate)); } @@ -2992,7 +3071,7 @@ private void givenAuctionResponseHook( AuctionInvocationContext, Future>> delegate) { - given(hookCatalog.hookById(eq(moduleCode), eq(hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE))) + given(hookCatalog.hookById(eqHook(moduleCode, hookImplCode), eq(StageWithHookType.AUCTION_RESPONSE))) .willReturn(AuctionResponseHookImpl.of(delegate)); } @@ -3013,6 +3092,10 @@ private BiFunction Future.succeededFuture(result); } + private static HookId eqHook(String moduleCode, String hookCode) { + return ArgumentMatchers.eq(HookId.of(moduleCode, hookCode)); + } + private HookStageExecutor createExecutor(String hostExecutionPlan) { return createExecutor(hostExecutionPlan, null); } diff --git a/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java new file mode 100644 index 00000000000..fa6f3291267 --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookProviderTest.java @@ -0,0 +1,132 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.VertxTest; +import org.prebid.server.hooks.execution.model.ABTest; +import org.prebid.server.hooks.execution.model.ExecutionAction; +import org.prebid.server.hooks.execution.model.GroupExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookExecutionContext; +import org.prebid.server.hooks.execution.model.HookExecutionOutcome; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.execution.provider.HookProvider; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.model.Endpoint; + +import static java.util.Collections.emptyList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ABTestHookProviderTest extends VertxTest { + + @Mock + private HookProvider innerHookProvider; + + @Mock + private Hook innerHook; + + @BeforeEach + public void setUp() { + given(innerHookProvider.apply(any())).willReturn(innerHook); + } + + @Test + public void applyShouldReturnOriginalHookIfNoABTestFound() { + // given + final HookProvider target = new ABTestHookProvider<>( + innerHookProvider, + singletonList(ABTest.builder().moduleCode("otherModule").build()), + HookExecutionContext.of(Endpoint.openrtb2_auction), + mapper); + + // when + final Hook result = target.apply(hookId()); + + // then + verify(innerHookProvider).apply(any()); + verifyNoInteractions(innerHook); + assertThat(result).isSameAs(innerHook); + } + + @Test + public void applyShouldReturnWrappedHook() { + // given + final HookProvider target = new ABTestHookProvider<>( + innerHookProvider, + singletonList(ABTest.builder().moduleCode("module").build()), + HookExecutionContext.of(Endpoint.openrtb2_auction), + mapper); + + // when + final Hook result = target.apply(hookId()); + + // then + verify(innerHookProvider).apply(any()); + verifyNoInteractions(innerHook); + assertThat(result).isInstanceOf(ABTestHook.class); + } + + @Test + public void shouldInvokeHookShouldReturnTrueIfThereIsAPreviousInvocation() { + // given + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + hookExecutionContext.getStageOutcomes().put(Stage.entrypoint, singletonList( + StageExecutionOutcome.of("entity", singletonList(GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(hookId()) + .action(ExecutionAction.update) + .build())))))); + + final ABTestHookProvider target = new ABTestHookProvider<>( + innerHookProvider, + emptyList(), + hookExecutionContext, + mapper); + + // when and then + verifyNoInteractions(innerHookProvider); + verifyNoInteractions(innerHook); + assertThat(target.shouldInvokeHook("module", null)).isTrue(); + } + + @Test + public void shouldInvokeHookShouldReturnFalseIfThereIsAPreviousExecutionWithoutInvocation() { + // given + final HookExecutionContext hookExecutionContext = HookExecutionContext.of(Endpoint.openrtb2_auction); + hookExecutionContext.getStageOutcomes().put(Stage.entrypoint, singletonList( + StageExecutionOutcome.of("entity", singletonList(GroupExecutionOutcome.of(singletonList( + HookExecutionOutcome.builder() + .hookId(hookId()) + .action(ExecutionAction.no_invocation) + .build())))))); + + final ABTestHookProvider target = new ABTestHookProvider<>( + innerHookProvider, + emptyList(), + hookExecutionContext, + mapper); + + // when and then + verifyNoInteractions(innerHookProvider); + verifyNoInteractions(innerHook); + assertThat(target.shouldInvokeHook("module", null)).isFalse(); + } + + private static HookId hookId() { + return HookId.of("module", "hook"); + } +} diff --git a/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java new file mode 100644 index 00000000000..f0d129cb5db --- /dev/null +++ b/src/test/java/org/prebid/server/hooks/execution/provider/abtest/ABTestHookTest.java @@ -0,0 +1,183 @@ +package org.prebid.server.hooks.execution.provider.abtest; + +import io.vertx.core.Future; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.VertxTest; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; +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.InvocationResultUtils; +import org.prebid.server.hooks.v1.InvocationStatus; + +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.prebid.server.hooks.v1.PayloadUpdate.identity; + +@ExtendWith(MockitoExtension.class) +public class ABTestHookTest extends VertxTest { + + @Mock + private Hook innerHook; + + @Mock + private Object payload; + + @Mock + private InvocationContext invocationContext; + + @Test + public void codeShouldReturnSameHookCode() { + // given + given(innerHook.code()).willReturn("code"); + + final Hook target = new ABTestHook<>( + "module", + innerHook, + false, + false, + mapper); + + // when and then + assertThat(target.code()).isEqualTo("code"); + } + + @Test + public void callShouldReturnSkippedResultWithoutTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + false, + false, + mapper); + + // when + final InvocationResult invocationResult = target.call(payload, invocationContext).result(); + + // then + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation); + assertThat(invocationResult.analyticsTags()).isNull(); + } + + @Test + public void callShouldReturnSkippedResultWithTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + false, + true, + mapper); + + // when + final InvocationResult invocationResult = target.call(payload, invocationContext).result(); + + // then + assertThat(invocationResult.status()).isEqualTo(InvocationStatus.success); + assertThat(invocationResult.action()).isEqualTo(InvocationAction.no_invocation); + assertThat(invocationResult.analyticsTags().activities()).hasSize(1).allSatisfy(activity -> { + assertThat(activity.name()).isEqualTo("core-module-abtests"); + assertThat(activity.status()).isEqualTo("success"); + assertThat(activity.results()).hasSize(1).allSatisfy(result -> { + assertThat(result.status()).isEqualTo("skipped"); + assertThat(result.values()).isEqualTo(mapper.createObjectNode().put("module", "module")); + }); + }); + } + + @Test + public void callShouldReturnRunResultWithoutTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + true, + false, + mapper); + + final InvocationResult innerHookInvocationResult = spy(InvocationResultUtils.succeeded(identity())); + final Future> innerHookResult = Future.succeededFuture(innerHookInvocationResult); + given(innerHook.call(any(), any())).willReturn(innerHookResult); + + // when + final Future> result = target.call(payload, invocationContext); + + // then + verify(innerHook).call(same(payload), same(invocationContext)); + verifyNoInteractions(innerHookInvocationResult); + assertThat(result).isSameAs(innerHookResult); + } + + @Test + public void callShouldReturnRunResultWithTags() { + // given + final Hook target = new ABTestHook<>( + "module", + innerHook, + true, + true, + mapper); + + final InvocationResult innerHookInvocationResult = spy(InvocationResultImpl.builder() + .status(InvocationStatus.success) + .message("message") + .action(InvocationAction.update) + .payloadUpdate(identity()) + .errors(singletonList("error")) + .warnings(singletonList("warning")) + .debugMessages(singletonList("debugMessages")) + .moduleContext(new Object()) + .analyticsTags(TagsImpl.of(asList( + ActivityImpl.of("activity0", null, null), + ActivityImpl.of("activity1", null, null)))) + .build()); + given(innerHook.call(any(), any())).willReturn(Future.succeededFuture(innerHookInvocationResult)); + + // when + final Future> result = target.call(payload, invocationContext); + + // then + verify(innerHook).call(same(payload), same(invocationContext)); + verifyNoInteractions(innerHookInvocationResult); + + final InvocationResult invocationResult = result.result(); + assertThat(invocationResult.status()).isSameAs(innerHookInvocationResult.status()); + assertThat(invocationResult.message()).isSameAs(innerHookInvocationResult.message()); + assertThat(invocationResult.action()).isSameAs(innerHookInvocationResult.action()); + assertThat(invocationResult.payloadUpdate()).isSameAs(innerHookInvocationResult.payloadUpdate()); + assertThat(invocationResult.errors()).isSameAs(innerHookInvocationResult.errors()); + assertThat(invocationResult.warnings()).isSameAs(innerHookInvocationResult.warnings()); + assertThat(invocationResult.debugMessages()).isSameAs(innerHookInvocationResult.debugMessages()); + assertThat(invocationResult.moduleContext()).isSameAs(innerHookInvocationResult.moduleContext()); + assertThat(invocationResult.analyticsTags().activities()).satisfies(activities -> { + for (int i = 0; i < activities.size() - 1; i++) { + assertThat(activities.get(i)).isSameAs(innerHookInvocationResult.analyticsTags().activities().get(i)); + } + + assertThat(activities.getLast()).satisfies(activity -> { + assertThat(activity.name()).isEqualTo("core-module-abtests"); + assertThat(activity.status()).isEqualTo("success"); + assertThat(activity.results()).hasSize(1).allSatisfy(activityResult -> { + assertThat(activityResult.status()).isEqualTo("run"); + assertThat(activityResult.values()) + .isEqualTo(mapper.createObjectNode().put("module", "module")); + }); + }); + }); + } +} diff --git a/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java index ce3e9ca74cb..71d21ce9596 100644 --- a/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java +++ b/src/test/java/org/prebid/server/hooks/v1/InvocationResultUtils.java @@ -1,5 +1,7 @@ package org.prebid.server.hooks.v1; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; + public class InvocationResultUtils { private InvocationResultUtils() { diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java deleted file mode 100644 index 0965bef2b40..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/ActivityImpl.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ActivityImpl implements Activity { - - String name; - - String status; - - List results; -} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java deleted file mode 100644 index 810313936a8..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/AppliedToImpl.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import lombok.Builder; -import lombok.Value; -import lombok.experimental.Accessors; - -import java.util.List; - -@Accessors(fluent = true) -@Builder -@Value -public class AppliedToImpl implements AppliedTo { - - List impIds; - - List bidders; - - boolean request; - - boolean response; - - List bidIds; -} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java deleted file mode 100644 index 3558a22c3cd..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/ResultImpl.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; -import lombok.experimental.Accessors; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class ResultImpl implements Result { - - String status; - - ObjectNode values; - - AppliedTo appliedTo; -} diff --git a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java b/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java deleted file mode 100644 index 92278f2469c..00000000000 --- a/src/test/java/org/prebid/server/hooks/v1/analytics/TagsImpl.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.v1.analytics; - -import lombok.Value; -import lombok.experimental.Accessors; - -import java.util.List; - -@Accessors(fluent = true) -@Value(staticConstructor = "of") -public class TagsImpl implements Tags { - - List activities; -} diff --git a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java index c1054166dc1..f843083b0fb 100644 --- a/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java +++ b/src/test/java/org/prebid/server/it/hooks/SampleItRawAuctionRequestHook.java @@ -4,15 +4,15 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; +import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; +import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.v1.InvocationAction; 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.analytics.ActivityImpl; -import org.prebid.server.hooks.v1.analytics.AppliedToImpl; -import org.prebid.server.hooks.v1.analytics.ResultImpl; -import org.prebid.server.hooks.v1.analytics.TagsImpl; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook;