From cb94b6835a6df8ea9efc858e2d18479e6e3f905a Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Fri, 28 Feb 2025 19:07:44 +0100 Subject: [PATCH 01/41] New module: optable-targeting --- extra/bundle/pom.xml | 5 + extra/modules/optable-targeting/README.md | 0 extra/modules/optable-targeting/lombok.config | 1 + extra/modules/optable-targeting/pom.xml | 77 +++++++ .../sample-requests/data.json | 137 +++++++++++ .../config/OptableTargetingConfig.java | 127 ++++++++++ .../targeting/model/EnrichmentStatus.java | 15 ++ .../modules/optable/targeting/model/Id.java | 48 ++++ .../optable/targeting/model/Metrics.java | 7 + .../targeting/model/ModuleContext.java | 28 +++ .../modules/optable/targeting/model/OS.java | 20 ++ .../targeting/model/OptableAttributes.java | 28 +++ .../optable/targeting/model/Reason.java | 17 ++ .../optable/targeting/model/Status.java | 16 ++ .../config/OptableTargetingProperties.java | 24 ++ .../targeting/model/net/HttpRequest.java | 16 ++ .../targeting/model/net/HttpResponse.java | 16 ++ .../targeting/model/net/OptableCall.java | 27 +++ .../targeting/model/net/OptableError.java | 30 +++ .../targeting/model/openrtb/Audience.java | 19 ++ .../targeting/model/openrtb/AudienceId.java | 9 + .../optable/targeting/model/openrtb/Data.java | 13 ++ .../model/openrtb/ExtUserOptable.java | 24 ++ .../targeting/model/openrtb/Ortb2.java | 9 + .../targeting/model/openrtb/Segment.java | 12 + .../model/openrtb/TargetingResult.java | 15 ++ .../optable/targeting/model/openrtb/User.java | 14 ++ .../OptableTargetingAuctionResponseHook.java | 98 ++++++++ .../targeting/v1/OptableTargetingModule.java | 18 ++ ...eTargetingProcessedAuctionRequestHook.java | 171 ++++++++++++++ .../v1/analytics/AnalyticTagsResolver.java | 26 +++ .../v1/analytics/AnalyticsTagsBuilder.java | 68 ++++++ .../v1/core/AuctionResponseValidator.java | 58 +++++ .../optable/targeting/v1/core/IdsMapper.java | 76 ++++++ .../targeting/v1/core/IdsResolver.java | 118 ++++++++++ .../optable/targeting/v1/core/IpResolver.java | 34 +++ .../v1/core/OptableAttributesResolver.java | 126 ++++++++++ .../targeting/v1/core/OptableTargeting.java | 26 +++ .../targeting/v1/core/PayloadResolver.java | 71 ++++++ .../targeting/v1/core/QueryBuilder.java | 83 +++++++ .../targeting/v1/core/merger/BaseMerger.java | 20 ++ .../v1/core/merger/BidRequestBuilder.java | 101 ++++++++ .../v1/core/merger/BidResponseBuilder.java | 84 +++++++ .../targeting/v1/core/merger/DataMerger.java | 101 ++++++++ .../targeting/v1/core/merger/EidsMerger.java | 71 ++++++ .../v1/core/merger/PayloadCleaner.java | 14 ++ .../optable/targeting/v1/net/APIClient.java | 160 +++++++++++++ .../v1/net/OptableHttpClientWrapper.java | 48 ++++ .../v1/net/OptableResponseParser.java | 21 ++ .../optable/targeting/v1/BaseOptableTest.java | 170 ++++++++++++++ ...tableTargetingAuctionResponseHookTest.java | 140 +++++++++++ .../v1/OptableTargetingModuleTest.java | 41 ++++ ...getingProcessedAuctionRequestHookTest.java | 174 ++++++++++++++ .../v1/core/AuctionResponseValidatorTest.java | 77 +++++++ .../targeting/v1/core/IdMapperTest.java | 127 ++++++++++ .../core/OptableAttributesResolverTest.java | 217 ++++++++++++++++++ .../v1/core/OptableTargetingTest.java | 115 ++++++++++ .../v1/core/PayloadResolverTest.java | 156 +++++++++++++ .../targeting/v1/core/QueryBuilderTest.java | 80 +++++++ .../v1/core/merger/BaseMergerTest.java | 18 ++ .../v1/core/merger/DataMergerTest.java | 151 ++++++++++++ .../v1/core/merger/EidsMergerTest.java | 133 +++++++++++ .../v1/core/merger/ExtMergerTest.java | 73 ++++++ .../targeting/v1/net/APIClientTest.java | 209 +++++++++++++++++ .../v1/net/OptableHttpClientWrapperTest.java | 114 +++++++++ .../v1/net/OptableResponseParserTest.java | 96 ++++++++ .../src/test/resources/error_response.json | 1 + .../test/resources/plaint_text_response.json | 1 + .../test/resources/targeting_response.json | 62 +++++ extra/modules/pom.xml | 1 + sample/configs/prebid-config-optable.yaml | 91 ++++++++ .../configs/sample-app-settings-optable.yaml | 17 ++ sample/stored/optable-stored-response.json | 166 ++++++++++++++ 73 files changed, 4777 insertions(+) create mode 100644 extra/modules/optable-targeting/README.md create mode 100644 extra/modules/optable-targeting/lombok.config create mode 100644 extra/modules/optable-targeting/pom.xml create mode 100644 extra/modules/optable-targeting/sample-requests/data.json create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java create mode 100644 extra/modules/optable-targeting/src/test/resources/error_response.json create mode 100644 extra/modules/optable-targeting/src/test/resources/plaint_text_response.json create mode 100644 extra/modules/optable-targeting/src/test/resources/targeting_response.json create mode 100644 sample/configs/prebid-config-optable.yaml create mode 100644 sample/configs/sample-app-settings-optable.yaml create mode 100644 sample/stored/optable-stored-response.json diff --git a/extra/bundle/pom.xml b/extra/bundle/pom.xml index ee0e136c0f4..8428ca7ad4a 100644 --- a/extra/bundle/pom.xml +++ b/extra/bundle/pom.xml @@ -55,6 +55,11 @@ pb-request-correction ${project.version} + + org.prebid.server.hooks.modules + optable-targeting + ${project.version} + diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/extra/modules/optable-targeting/lombok.config b/extra/modules/optable-targeting/lombok.config new file mode 100644 index 00000000000..efd92714219 --- /dev/null +++ b/extra/modules/optable-targeting/lombok.config @@ -0,0 +1 @@ +lombok.anyConstructor.addConstructorProperties = true diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml new file mode 100644 index 00000000000..010eb5e8383 --- /dev/null +++ b/extra/modules/optable-targeting/pom.xml @@ -0,0 +1,77 @@ + + + 4.0.0 + + + org.prebid.server.hooks.modules + all-modules + 3.22.0-SNAPSHOT + + + optable-targeting + + optable-targeting + Optable targeting module + + + + + 3.4.0 + 10.17.0 + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + ${checkstyle-plugin.version} + + + ../checkstyle.xml + UTF-8 + true + + true + false + true + + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + + + + + + org.apache.maven.plugins + maven-checkstyle-plugin + + + validate + + checkstyle + + + + + + + + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + + + com.puppycrawl.tools + checkstyle + ${checkstyle.version} + + + diff --git a/extra/modules/optable-targeting/sample-requests/data.json b/extra/modules/optable-targeting/sample-requests/data.json new file mode 100644 index 00000000000..8ce26abedb6 --- /dev/null +++ b/extra/modules/optable-targeting/sample-requests/data.json @@ -0,0 +1,137 @@ +{ + "test": 1, + "id": "1", + "imp": + [ + { + "id": "1", + "banner": + { + "w": 300, + "h": 250 + }, + "ext": + { + "prebid": + { + "storedauctionresponse": { "id": "optable-stored-response" }, + "bidder": + { + "appnexus": + { + "placementId": 0 + } + } + } + } + } + ], + "site": + { + "domain": "test.com", + "publisher": + { + "domain": "test.com", + "id": "1" + }, + "page": "https://www.test.com/" + }, + "device": + { + "ip": "8.8.8.8" + }, + "user": + { + "ext": + { + "optable": + { + "email": "fd911bd8cac2e603a80efafca2210b7a917c97410f0c29d9f2bfb99867e5a589" + }, + "eids": + [ + { + "source": "growthcode.io", + "uids": + [ + { + "id": "fb58593e-7ac6-48bd-b2de-89a758726362", + "atype": 1 + } + ] + }, + { + "source": "pubcid.org", + "uids": [ + { + "id": "f9528392fd38786082c7fd01cd2aa5a60d2c3a2788789b1f9de4574a7c9dabae", + "atype": 1 + } + ] + }, + { + "source": "crwdcntrl.net", + "uids": + [ + { + "id": "dd1b31e65f5e45548c11a0275ba3a8072c00e3a2a0493e8f5a8f54f8067e8b00", + "atype": 1 + } + ] + }, + { + "source": "id5-sync.com", + "uids": + [ + { + "id": "ID5*dd1b31e65f5e45548c11a0275ba3a8072c00e3a2a0493e8f5a8f54f8067e8b00", + "atype": 1 + } + ] + }, + { + "source": "amxdt.net", + "uids": + [ + { + "id": "amx*3*a583802a-e6fe-48d7-87c6-7db1b6a4a73a*70f06cdcf8ab0b4ac07a56860ed0e0b6ef0388dc0b0ab5a1dd725999d3b339cf", + "atype": 1 + } + ] + }, + { + "source": "audigent.com", + "uids": + [ + { + "id": "f84456cd3c72296d7898f62e1c46dd964206ff4d47e64b690c3c5a1d6b1bd286", + "atype": 1 + } + ] + }, + { + "source": "adnxs.com", + "uids": + [ + { + "id": "d4fd63f0f4f7ce0d128348cb145c7e0f" + } + ] + } + ] + } + }, + "ext": { + "prebid": { + "targeting": { + "includebidderkeys": true + }, + "analytics": + { + "options": { + "enableclientdetails": true + } + } + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java new file mode 100644 index 00000000000..b853f0d36fb --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -0,0 +1,127 @@ +package org.prebid.server.hooks.modules.optable.targeting.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Vertx; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.IpResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseParser; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.spring.config.VertxContextScope; +import org.prebid.server.spring.config.model.HttpClientProperties; +import org.prebid.server.util.HttpUtil; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Scope; +import org.springframework.context.annotation.ScopedProxyMode; + +import java.util.List; +import java.util.Objects; + +@ConditionalOnProperty(prefix = "hooks." + OptableTargetingModule.CODE, name = "enabled", havingValue = "true") +@Configuration +@EnableConfigurationProperties(OptableTargetingProperties.class) +public class OptableTargetingConfig { + + @Bean + ObjectMapper objectMapper() { + return new ObjectMapper(); + } + + @Bean + AnalyticTagsResolver analyticTagsResolver(ObjectMapper objectMapper) { + return new AnalyticTagsResolver(objectMapper); + } + + @Bean + IdsMapper queryParametersExtractor(OptableTargetingProperties properties, ObjectMapper objectMapper) { + return new IdsMapper(objectMapper, properties.getPpidMapping()); + } + + @Bean + PayloadResolver payloadResolver(ObjectMapper mapper) { + return new PayloadResolver(mapper); + } + + @Bean + QueryBuilder queryBuilder(OptableTargetingProperties properties) { + return new QueryBuilder(properties.getIdPrefixOrder()); + } + + @Bean + OptableResponseParser optableResponseParser(JacksonMapper mapper) { + return new OptableResponseParser(mapper); + } + + @Bean + @Scope(scopeName = VertxContextScope.NAME, proxyMode = ScopedProxyMode.TARGET_CLASS) + @ConditionalOnProperty(prefix = "http-client.circuit-breaker", name = "enabled", havingValue = "false", + matchIfMissing = true) + OptableHttpClientWrapper optableHttpClient(Vertx vertx, HttpClientProperties httpClientProperties) { + return new OptableHttpClientWrapper(vertx, httpClientProperties); + } + + @Bean + APIClient apiClient(OptableHttpClientWrapper httpClientWrapper, + @Value("${logging.sampling-rate:0.01}") + double logSamplingRate, + OptableTargetingProperties properties, + OptableResponseParser responseParser) { + + final String endpoint = HttpUtil.validateUrl(Objects.requireNonNull(properties.getApiEndpoint())); + + return new APIClient(endpoint, httpClientWrapper.getHttpClient(), logSamplingRate, responseParser, + properties.getApiKey()); + } + + @Bean + OptableAttributesResolver optableAttributesResolver(IpResolver ipResolver) { + return new OptableAttributesResolver(ipResolver); + } + + @Bean() + OptableTargeting optableTargeting(IdsMapper parametersExtractor, + QueryBuilder queryBuilder, APIClient apiClient) { + + return new OptableTargeting(parametersExtractor, queryBuilder, apiClient); + } + + @Bean + IpResolver ipResolver() { + return new IpResolver(); + } + + @Bean + AuctionResponseValidator auctionResponseValidator() { + return new AuctionResponseValidator(); + } + + @Bean + OptableTargetingModule optableTargetingModule(OptableTargetingProperties properties, + AnalyticTagsResolver analyticTagsResolver, + OptableTargeting optableTargeting, + PayloadResolver payloadResolver, + OptableAttributesResolver optableAttributesResolver, + AuctionResponseValidator auctionResponseValidator) { + + return new OptableTargetingModule(List.of( + new OptableTargetingProcessedAuctionRequestHook(properties, optableTargeting, payloadResolver, + optableAttributesResolver), + new OptableTargetingAuctionResponseHook(analyticTagsResolver, payloadResolver, + properties.getAdserverTargeting(), auctionResponseValidator))); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java new file mode 100644 index 00000000000..08affde67f1 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Builder; + +@Builder(toBuilder = true) +public record EnrichmentStatus(Status status, Reason reason) { + + public static EnrichmentStatus fail() { + return EnrichmentStatus.builder().status(Status.FAIL).build(); + } + + public static EnrichmentStatus success() { + return EnrichmentStatus.builder().status(Status.SUCCESS).build(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java new file mode 100644 index 00000000000..ebd0d53ae05 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Value; + +import javax.validation.constraints.NotNull; + +@AllArgsConstructor(staticName = "of") +@Builder(toBuilder = true) +@Value +public class Id { + + public static final String EMAIL = "e"; + + public static final String PHONE = "p"; + + public static final String ZIP = "z"; + + public static final String DEVICE_IP_V_4 = "i4"; + + public static final String DEVICE_IP_V_6 = "i6"; + + public static final String APPLE_IDFA = "a"; + + public static final String GOOGLE_GAID = "g"; + + public static final String ROKU_RIDA = "r"; + + public static final String SAMSUNG_TV_TIFA = "s"; + + public static final String AMAZON_FIRE_AFAI = "f"; + + public static final String NET_ID = "n"; + + public static final String ID5 = "id5"; + + public static final String UTIQ = "utiq"; + + public static final String OPTABLE_VID = "v"; + + /** Name of Identifier */ + @NotNull + String name; + + /** Identifier's value */ + String value; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java new file mode 100644 index 00000000000..04788ecbc84 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java @@ -0,0 +1,7 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Builder; + +@Builder(toBuilder = true) +public record Metrics(long moduleStartTime, long moduleFinishTime) { +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java new file mode 100644 index 00000000000..f58b0d6a4ee --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Getter; +import lombok.Setter; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +import java.util.List; +import java.util.Objects; + +@Setter +@Getter +public class ModuleContext { + + private Metrics metrics; + + private List targeting; + + private EnrichmentStatus enrichRequestStatus; + + private EnrichmentStatus enrichResponseStatus; + + private boolean adserverTargetingEnabled; + + public static ModuleContext of(AuctionInvocationContext invocationContext) { + return (ModuleContext) Objects.requireNonNull(invocationContext.moduleContext()); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java new file mode 100644 index 00000000000..7f2388f5614 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java @@ -0,0 +1,20 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +public enum OS { + + IOS("ios"), + + ANDROID("android"), + + ROKU("roku"), + + TIZEN("tizen"), + + FIRE("fire"); + + public final String value; + + OS(String value) { + this.value = value; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java new file mode 100644 index 00000000000..967e53dd544 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -0,0 +1,28 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Builder; +import lombok.Value; + +import java.util.List; +import java.util.Set; + +@Value +@Builder(toBuilder = true) +public class OptableAttributes { + + String reg; + + String gpp; + + Set gppSid; + + String tcf; + + List ips; + + Long timeout; + + public static OptableAttributes of(String reg) { + return OptableAttributes.builder().reg(reg).build(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java new file mode 100644 index 00000000000..b5b13f69364 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Reason.java @@ -0,0 +1,17 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Getter; + +public enum Reason { + + NONE("none"), + NOBID("nobid"), + NOKEYWORD("nokeyword"); + + @Getter + private final String value; + + Reason(String value) { + this.value = value; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java new file mode 100644 index 00000000000..dcc03428a7a --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Status.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Getter; + +public enum Status { + + SUCCESS("success"), + FAIL("fail"); + + @Getter + private final String value; + + Status(String value) { + this.value = value; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java new file mode 100644 index 00000000000..bce2f4ec1f9 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.config; + +import lombok.Data; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +@ConfigurationProperties(prefix = "hooks.modules." + OptableTargetingModule.CODE) +@Data +public final class OptableTargetingProperties { + + String apiEndpoint; + + String apiKey; + + Map ppidMapping; + + Boolean adserverTargeting = true; + + Long timeout; + + String idPrefixOrder; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java new file mode 100644 index 00000000000..b9188bb1027 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.net; + +import io.vertx.core.MultiMap; +import lombok.Builder; +import lombok.Value; + +@Builder(toBuilder = true) +@Value +public class HttpRequest { + + String uri; + + String query; + + MultiMap headers; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java new file mode 100644 index 00000000000..35390f48e3f --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.net; + +import io.vertx.core.MultiMap; +import lombok.AllArgsConstructor; +import lombok.Value; + +@AllArgsConstructor(staticName = "of") +@Value +public class HttpResponse { + + int statusCode; + + MultiMap headers; + + String body; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java new file mode 100644 index 00000000000..d67e5e22818 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.net; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Value; + +@Value +@AllArgsConstructor(access = AccessLevel.PRIVATE) +public class OptableCall { + + HttpRequest request; + + HttpResponse response; + + OptableError error; + + public static OptableCall succeededHttp(HttpRequest request, + HttpResponse response, + OptableError error) { + + return new OptableCall(request, response, error); + } + + public static OptableCall failedHttp(HttpRequest request, OptableError error) { + return new OptableCall(request, null, error); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java new file mode 100644 index 00000000000..9f97eb6bf7d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.net; + +import lombok.Getter; +import lombok.Value; + +@Value(staticConstructor = "of") +public class OptableError { + + String message; + + Type type; + + @Getter + public enum Type { + + BAD_INPUT(2), + + BAD_SERVER_RESPONSE(3), + + TIMEOUT(4), + + GENERIC(5); + + private final Integer code; + + Type(final Integer errorCode) { + this.code = errorCode; + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java new file mode 100644 index 00000000000..8d793c5d5d7 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java @@ -0,0 +1,19 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Value; + +import java.util.List; + +@Value +public class Audience { + + String provider; + + List ids; + + String keyspace; + + @JsonProperty("rtb_segtax") + Integer rtbSegtax; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java new file mode 100644 index 00000000000..73d7953fbdc --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/AudienceId.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +@Value +public class AudienceId { + + String id; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java new file mode 100644 index 00000000000..7020cc430d2 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java @@ -0,0 +1,13 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +import java.util.List; + +@Value +public class Data { + + String id; + + List segment; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java new file mode 100644 index 00000000000..00281604b94 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java @@ -0,0 +1,24 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Value; +import org.prebid.server.proto.openrtb.ext.FlexibleExtension; + +/** + * An object containing identifiers + * which are provided by Optable + */ +@EqualsAndHashCode(callSuper = true) +@Builder(toBuilder = true) +@Value +public class ExtUserOptable extends FlexibleExtension { + + String email; + + String phone; + + String zip; + + String vid; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java new file mode 100644 index 00000000000..f8dace457af --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Ortb2.java @@ -0,0 +1,9 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import lombok.Value; + +@Value +public class Ortb2 { + + User user; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java new file mode 100644 index 00000000000..b66238a661d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java @@ -0,0 +1,12 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import lombok.Value; + +@Value +public class Segment { + + String id; + + ObjectNode ext; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java new file mode 100644 index 00000000000..c5ab2959117 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import lombok.Value; + +import java.util.List; + +@Value +@JsonIgnoreProperties(ignoreUnknown = true) +public class TargetingResult { + + List audience; + + Ortb2 ortb2; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java new file mode 100644 index 00000000000..92f4df77101 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; + +import com.iab.openrtb.request.Eid; +import lombok.Value; + +import java.util.List; + +@Value +public class User { + + List eids; + + List data; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java new file mode 100644 index 00000000000..d4addf7d4c6 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -0,0 +1,98 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +import java.util.List; +import java.util.function.Function; + +@AllArgsConstructor +public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { + + private static final String CODE = "optable-targeting-auction-response-hook"; + + private AnalyticTagsResolver analyticTagsResolver; + + private PayloadResolver payloadResolver; + + boolean adserverTargeting; + + private AuctionResponseValidator auctionResponseValidator; + + @Override + public String code() { + return CODE; + } + + @Override + public Future> call(AuctionResponsePayload auctionResponsePayload, + AuctionInvocationContext invocationContext) { + + final ModuleContext moduleContext = ModuleContext.of(invocationContext); + moduleContext.setAdserverTargetingEnabled(adserverTargeting); + + if (adserverTargeting) { + final EnrichmentStatus validationStatus = auctionResponseValidator.checkEnrichmentPossibility( + auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); + moduleContext.setEnrichResponseStatus(validationStatus); + + if (validationStatus.status() == Status.SUCCESS) { + return enrichedPayloadFuture(moduleContext); + } + } + + return successFeature(moduleContext); + } + + private Future> enrichedPayloadFuture(ModuleContext moduleContext) { + final List targeting = moduleContext.getTargeting(); + + return CollectionUtils.isNotEmpty(targeting) + ? updateFeature(payload -> enrichPayload(payload, targeting), moduleContext) + : successFeature(moduleContext); + } + + private AuctionResponsePayload enrichPayload(AuctionResponsePayload payload, List targeting) { + return AuctionResponsePayloadImpl.of(payloadResolver.enrichBidResponse(payload.bidResponse(), targeting)); + } + + private Future> updateFeature( + Function func, ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(func::apply) + .moduleContext(moduleContext) + .analyticsTags(analyticTagsResolver.resolve(moduleContext)) + .build()); + } + + private Future> successFeature(ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(moduleContext) + .analyticsTags(analyticTagsResolver.resolve(moduleContext)) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java new file mode 100644 index 00000000000..32fccda8375 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; + +public record OptableTargetingModule( + Collection> hooks) implements Module { + + public static final String CODE = "optable-targeting"; + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java new file mode 100644 index 00000000000..9d6279073ca --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -0,0 +1,171 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import lombok.AllArgsConstructor; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; + +import java.util.Optional; +import java.util.function.Function; + +@AllArgsConstructor +public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { + + private static final String CODE = "optable-targeting-processed-auction-request-hook"; + + private static final long DEFAULT_API_CALL_TIMEOUT = 1000L; + + private final OptableTargetingProperties properties; + + private OptableTargeting optableTargeting; + + private PayloadResolver payloadResolver; + + private final OptableAttributesResolver optableAttributesResolver; + + @Override + public String code() { + return CODE; + } + + @Override + public Future> call(AuctionRequestPayload auctionRequestPayload, + AuctionInvocationContext invocationContext) { + + final ModuleContext moduleContext = createModuleContext(); + + final BidRequest bidRequest = getBidRequest(auctionRequestPayload); + if (bidRequest == null) { + return failedFeature(moduleContext); + } + + final long timeout = getHookRemainTime(invocationContext); + final OptableAttributes attributes = optableAttributesResolver.reloveAttributes( + invocationContext.auctionContext(), + properties.getTimeout()); + + final Future targetingResultFuture = optableTargeting.getTargeting( + bidRequest, + attributes, + timeout); + + if (targetingResultFuture == null) { + return failedFeature(this::sanitizePayload, moduleContext); + } + + return targetingResultFuture.compose(targetingResult -> enrichedPayloadFuture(targetingResult, moduleContext)) + .recover(throwable -> failedFeature(this::sanitizePayload, moduleContext)); + } + + private ModuleContext createModuleContext() { + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setMetrics(Metrics.builder().moduleStartTime(System.currentTimeMillis()) + .build()); + + return moduleContext; + } + + private BidRequest getBidRequest(AuctionRequestPayload auctionRequestPayload) { + return Optional.ofNullable(auctionRequestPayload) + .map(AuctionRequestPayload::bidRequest) + .orElse(null); + } + + private long getHookRemainTime(AuctionInvocationContext invocationContext) { + return Optional.ofNullable(invocationContext) + .map(AuctionInvocationContext::timeout) + .map(Timeout::remaining) + .orElse(DEFAULT_API_CALL_TIMEOUT); + } + + private Future> enrichedPayloadFuture(TargetingResult targetingResult, + ModuleContext moduleContext) { + + moduleContext.setMetrics(moduleContext.getMetrics().toBuilder() + .moduleFinishTime(System.currentTimeMillis()) + .build()); + + if (targetingResult != null) { + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + + return updateFeature(payload -> { + final AuctionRequestPayload sanitizedPayload = sanitizePayload(payload); + return enrichPayload(sanitizedPayload, targetingResult); + }, moduleContext); + } else { + return failedFeature(this::sanitizePayload, moduleContext); + } + } + + private AuctionRequestPayload enrichPayload(AuctionRequestPayload payload, TargetingResult targetingResult) { + return AuctionRequestPayloadImpl.of(payloadResolver.enrichBidRequest(payload.bidRequest(), targetingResult)); + } + + private AuctionRequestPayload sanitizePayload(AuctionRequestPayload payload) { + return AuctionRequestPayloadImpl.of(payloadResolver.clearBidRequest(payload.bidRequest())); + } + + private Future> updateFeature( + Function func, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(func::apply) + .moduleContext(moduleContext) + .build()); + } + + private Future> failedFeature(ModuleContext moduleContext) { + moduleContext.setEnrichRequestStatus(EnrichmentStatus.fail()); + return succeededFeature(moduleContext); + } + + private Future> failedFeature( + Function func, + ModuleContext moduleContext) { + + moduleContext.setEnrichRequestStatus(EnrichmentStatus.fail()); + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .payloadUpdate(func::apply) + .moduleContext(moduleContext) + .build()); + } + + private Future> succeededFeature(ModuleContext moduleContext) { + moduleContext.setMetrics(moduleContext.getMetrics().toBuilder() + .moduleFinishTime(System.currentTimeMillis()) + .build()); + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(moduleContext) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java new file mode 100644 index 00000000000..34f6e086e70 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.v1.analytics.Tags; + +@AllArgsConstructor +public class AnalyticTagsResolver { + + private final ObjectMapper objectMapper; + + public Tags resolve(ModuleContext moduleContext) { + final Metrics metrics = moduleContext.getMetrics(); + + return AnalyticsTagsBuilder.builder() + .objectMapper(objectMapper) + .adserverTargeting(moduleContext.isAdserverTargetingEnabled()) + .requestEnrichmentStatus(moduleContext.getEnrichRequestStatus().status()) + .responseEnrichmentStatus(moduleContext.getEnrichResponseStatus()) + .requestEnrichmentExecutionTime(metrics.moduleFinishTime() - metrics.moduleStartTime()) + .build() + .buildTags(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java new file mode 100644 index 00000000000..7977a0434de --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java @@ -0,0 +1,68 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Builder; +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.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Tags; + +import java.util.ArrayList; +import java.util.List; + +@Builder(toBuilder = true) +public class AnalyticsTagsBuilder { + + private static final String STATUS_OK = "OK"; + + private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; + + private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; + + private static final String STATUS_EXECUTION_TIME = "execution-time"; + + private static final String STATUS_REASON = "reason"; + + private final ObjectMapper objectMapper; + + private final long requestEnrichmentExecutionTime; + + private final Status requestEnrichmentStatus; + + private final EnrichmentStatus responseEnrichmentStatus; + + private final boolean adserverTargeting; + + public Tags buildTags() { + final List activities = new ArrayList<>(); + activities.add(buildEnrichRequestActivity()); + + if (adserverTargeting) { + activities.add(buildEnrichResponseActivity()); + } + return TagsImpl.of(activities); + } + + private Activity buildEnrichRequestActivity() { + return ActivityImpl.of(ACTIVITY_ENRICH_REQUEST, + requestEnrichmentStatus != null + ? requestEnrichmentStatus.getValue() + : null, + List.of(ResultImpl.of(null, objectMapper.createObjectNode().put(STATUS_EXECUTION_TIME, + requestEnrichmentExecutionTime), null))); + } + + private Activity buildEnrichResponseActivity() { + return ActivityImpl.of(ACTIVITY_ENRICH_RESPONSE, + responseEnrichmentStatus != null + ? responseEnrichmentStatus.status().getValue() + : null, + List.of(ResultImpl.of(null, objectMapper.createObjectNode().put(STATUS_REASON, + responseEnrichmentStatus != null + ? responseEnrichmentStatus.reason().getValue() + : null), null))); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java new file mode 100644 index 00000000000..d45de6f0c23 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; + +import java.util.List; +import java.util.Optional; + +public class AuctionResponseValidator { + + public EnrichmentStatus checkEnrichmentPossibility(BidResponse bidResponse, List targeting) { + Status status = Status.SUCCESS; + Reason reason = Reason.NONE; + + if (!hasKeywords(targeting)) { + status = Status.FAIL; + reason = Reason.NOKEYWORD; + } else if (!hasBids(bidResponse)) { + status = Status.FAIL; + reason = Reason.NOBID; + } + + return EnrichmentStatus.builder() + .status(status) + .reason(reason) + .build(); + } + + private boolean hasKeywords(List targeting) { + if (CollectionUtils.isEmpty(targeting)) { + return false; + } + + final long idsCounter = targeting.stream() + .mapToLong(audience -> Optional.ofNullable(audience.getIds()).orElse(List.of()).size()) + .sum(); + + return idsCounter > 0; + } + + private boolean hasBids(BidResponse bidResponse) { + final List seatBids = Optional.ofNullable(bidResponse).map(BidResponse::getSeatbid).orElse(null); + if (CollectionUtils.isEmpty(seatBids)) { + return false; + } + + final long bidsCount = seatBids.stream() + .mapToLong(seatBid -> Optional.ofNullable(seatBid.getBid()).orElse(List.of()).size()) + .sum(); + + return bidsCount > 0; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java new file mode 100644 index 00000000000..8a08414aff1 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OS; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@AllArgsConstructor +public class IdsMapper { + + private final ObjectMapper objectMapper; + + private final Map ppidMapping; + + public List toIds(BidRequest bidRequest) { + final IdsResolver idsResolver = IdsResolver.of(objectMapper, bidRequest); + + final Map ids = applyStaticMapping(idsResolver); + final Map dynamicIds = applyDynamicMapping(idsResolver); + if (dynamicIds != null && !dynamicIds.isEmpty()) { + ids.putAll(dynamicIds); + } + + return ids.entrySet().stream() + .filter(it -> it.getValue() != null) + .map(it -> Id.of(it.getKey(), it.getValue())) + .toList(); + } + + private Map toIds(List eids) { + if (ppidMapping == null || ppidMapping.isEmpty() || CollectionUtils.isEmpty(eids)) { + return null; + } + + return eids.stream().map(it -> { + final String key = ppidMapping.get(it.getSource()); + + if (key != null) { + return Pair.of(key, it.getUids().stream().findFirst().map(Uid::getId).orElse(null)); + } + + return null; + }).filter(Objects::nonNull).collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + } + + private Map applyDynamicMapping(IdsResolver idsResolver) { + return toIds(idsResolver.getEIDs()); + } + + private Map applyStaticMapping(IdsResolver idsResolver) { + return new HashMap<>() {{ + put(Id.EMAIL, idsResolver.getEmail()); + put(Id.PHONE, idsResolver.getPhone()); + put(Id.ZIP, idsResolver.getZip()); + put(Id.APPLE_IDFA, idsResolver.getDeviceIfa(OS.IOS)); + put(Id.GOOGLE_GAID, idsResolver.getDeviceIfa(OS.ANDROID)); + put(Id.ROKU_RIDA, idsResolver.getDeviceIfa(OS.ROKU)); + put(Id.SAMSUNG_TV_TIFA, idsResolver.getDeviceIfa(OS.TIZEN)); + put(Id.AMAZON_FIRE_AFAI, idsResolver.getDeviceIfa(OS.FIRE)); + put(Id.NET_ID, idsResolver.getNetId()); + put(Id.ID5, idsResolver.getID5()); + put(Id.UTIQ, idsResolver.getUtiq()); + put(Id.OPTABLE_VID, idsResolver.getOptableVID()); }}; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java new file mode 100644 index 00000000000..c2852104f6c --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java @@ -0,0 +1,118 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.OS; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.List; +import java.util.Optional; + +public class IdsResolver { + + private final Optional bidRequest; + private final ObjectMapper objectMapper; + private final Optional extUser; + private final Optional extUserOptable; + private final Optional device; + + public static IdsResolver of(ObjectMapper objectMapper, BidRequest bidRequest) { + return new IdsResolver(objectMapper, bidRequest); + } + + private IdsResolver(ObjectMapper objectMapper, BidRequest bidRequest) { + this.objectMapper = objectMapper; + this.bidRequest = Optional.ofNullable(bidRequest); + this.extUser = getExtUser(); + this.extUserOptable = getExtUserOptable(); + this.device = this.bidRequest.map(BidRequest::getDevice); + } + + public String getEmail() { + return extUserOptable.map(ExtUserOptable::getEmail).orElse(null); + } + + public String getPhone() { + return extUserOptable.map(ExtUserOptable::getPhone).orElse(null); + } + + public String getZip() { + return extUserOptable.map(ExtUserOptable::getZip).orElse(null); + } + + public String getOptableVID() { + return extUserOptable.map(ExtUserOptable::getVid).orElse(null); + } + + public String getDeviceIfa(OS os) { + final String deviceOS = getDeviceOS(); + final Integer deviceLmt = getDeviceLmt(); + + if (StringUtils.isEmpty(deviceOS)) { + return null; + } + + if (deviceOS.contains(os.value.toLowerCase()) && !(deviceLmt != null && deviceLmt.equals(1))) { + return device.map(Device::getIfa).orElse(null); + } + + return null; + } + + public String getID5() { + return getUid("id5-sync.com"); + } + + public String getUtiq() { + return getUid("utiq.com"); + } + + public String getNetId() { + return getUid("netid.de"); + } + + public List getEIDs() { + return bidRequest.map(BidRequest::getUser).map(User::getEids).orElse(List.of()); + } + + private Optional getExtUser() { + return bidRequest.map(BidRequest::getUser).map(User::getExt); + } + + private Integer getDeviceLmt() { + return device.map(Device::getLmt).orElse(null); + } + + private String getDeviceOS() { + return device.map(Device::getOs).map(String::toLowerCase).orElse(null); + } + + private String getUid(String source) { + return getEIDs() + .stream() + .filter(it -> it.getSource().equals(source)) + .findFirst() + .map(Eid::getUids) + .flatMap(it -> it.stream().findFirst()) + .map(Uid::getId) + .orElse(null); + } + + private Optional getExtUserOptable() { + return this.extUser.map(it -> it.getProperty("optable")) + .map(it -> { + try { + return objectMapper.treeToValue(it, ExtUserOptable.class); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + }); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java new file mode 100644 index 00000000000..4a6eee8f90c --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.privacy.model.PrivacyContext; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +public class IpResolver { + + public List resolveIp(AuctionContext auctionContext) { + final List result = new ArrayList<>(); + + final Optional auctionContextOpt = Optional.ofNullable(auctionContext); + + final Optional deviceOpt = auctionContextOpt + .map(AuctionContext::getBidRequest) + .map(BidRequest::getDevice); + + deviceOpt.map(Device::getIp).ifPresent(result::add); + deviceOpt.map(Device::getIpv6).ifPresent(result::add); + + if (result.isEmpty()) { + auctionContextOpt.map(AuctionContext::getPrivacyContext) + .map(PrivacyContext::getIpAddress) + .ifPresent(result::add); + } + + return result; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java new file mode 100644 index 00000000000..2989943aa96 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -0,0 +1,126 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.gpp.encoder.GppModel; +import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.auction.gpp.model.GppContext; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.privacy.model.PrivacyContext; + +import java.util.List; +import java.util.Optional; +import java.util.Set; + +@AllArgsConstructor +public class OptableAttributesResolver { + + private final IpResolver ipResolver; + + private static final int SECTION_ID_CANADA = 5; + + private static final Set SECTION_ID_EUROPE = Set.of(1, 2); + + private static final Set SECTION_ID_US = + Set.of(6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22); + + private static final Set GDPR_COUNTRIES = Set.of("Belgium", "Bulgaria", "Cyprus", "Denmark", + "Germany", "Estonia", "Finland", "France", "Greece", "Hungary", "Ireland", "Italy", "Croatia", "Latvia", + "Liechtenstein", "Lithuania", "Luxembourg", "Malta", "The Netherlands", "Norway", + "Austria", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Switzerland", "UK"); + + private static final Set GDPR_REGIONS = Set.of("Azores", "Canary", "Islands", "Guadeloupe", "French Guiana", + "Madeira", "Martinique", "Mayotte", "Reunion", "Saint Martin"); + + public OptableAttributes reloveAttributes(AuctionContext auctionContext, Long timeout) { + final List ips = ipResolver.resolveIp(auctionContext); + + OptableAttributes optableAttributes = getTcfPrivacyAttributes(auctionContext); + if (optableAttributes == null) { + optableAttributes = getGppPrivacyAttributes(auctionContext); + } + if (optableAttributes == null) { + optableAttributes = getGeoIpPrivacyAttributes(auctionContext); + } + + return optableAttributes != null + ? optableAttributes.toBuilder().ips(ips).timeout(timeout).build() + : OptableAttributes.builder().ips(ips).timeout(timeout).build(); + } + + private OptableAttributes getGeoIpPrivacyAttributes(AuctionContext auctionContext) { + final Optional geoInfoOpt = Optional.ofNullable(auctionContext).map(AuctionContext::getGeoInfo); + + final String country = geoInfoOpt.map(GeoInfo::getCountry).orElse(null); + final String region = geoInfoOpt.map(GeoInfo::getRegion).orElse(null); + + if (StringUtils.isNotEmpty(country)) { + if (country.equalsIgnoreCase("US") || country.equalsIgnoreCase("United States")) { + return OptableAttributes.of("us"); + } else if (country.equalsIgnoreCase("Quebec") || country.equalsIgnoreCase("Canada")) { + return OptableAttributes.of("can"); + } else if (GDPR_COUNTRIES.contains(country)) { + return OptableAttributes.of("gdpr"); + } + } + + if (StringUtils.isNotEmpty(region) && GDPR_REGIONS.contains(region)) { + return OptableAttributes.of("gdpr"); + } + + return null; + } + + private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) { + final Optional gppContextOpt = Optional.ofNullable(auctionContext) + .map(AuctionContext::getGppContext); + + final Optional gppScope = gppContextOpt + .map(GppContext::scope); + + final String gppConsent = gppScope.map(GppContext.Scope::getGppModel) + .map(GppModel::encode) + .orElse(null); + + if (gppConsent != null) { + final Set sids = gppContextOpt + .map(GppContext::scope) + .map(GppContext.Scope::getSectionsIds) + .orElse(Set.of()); + + return OptableAttributes.of(sidsToReg(sids)).toBuilder().gpp(gppConsent).gppSid(sids).build(); + + } + + return null; + } + + private OptableAttributes getTcfPrivacyAttributes(AuctionContext auctionContext) { + return Optional.ofNullable(auctionContext) + .map(AuctionContext::getPrivacyContext) + .map(PrivacyContext::getTcfContext) + .map(ctx -> { + if (ctx.isConsentValid()) { + return OptableAttributes.of("gdpr").toBuilder().tcf(ctx.getConsentString()).build(); + } + return null; + }).orElse(null); + } + + private String sidsToReg(Set sids) { + if (sids == null) { + return null; + } + + if (sids.contains(SECTION_ID_CANADA)) { + return "can"; + } else if (sids.stream().anyMatch(SECTION_ID_EUROPE::contains)) { + return "gdpr"; + } else if (sids.stream().anyMatch(SECTION_ID_US::contains)) { + return "us"; + } + + return null; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java new file mode 100644 index 00000000000..203dccb3dcf --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import lombok.AllArgsConstructor; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; + +import java.util.Optional; + +@AllArgsConstructor +public class OptableTargeting { + + private final IdsMapper idsMapper; + private final QueryBuilder queryBuilder; + private final APIClient apiClient; + + public Future getTargeting(BidRequest bidRequest, OptableAttributes attributes, long timeout) { + return Optional.ofNullable(bidRequest) + .map(idsMapper::toIds) + .map(ids -> queryBuilder.build(ids, attributes)) + .map(query -> apiClient.getTargeting(query, attributes.getIps(), timeout)) + .orElse(Future.failedFuture("Can't get targeting")); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java new file mode 100644 index 00000000000..fb33574f10a --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java @@ -0,0 +1,71 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.response.BidResponse; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidRequestBuilder; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidResponseBuilder; + +import java.util.List; +import java.util.Optional; + +@AllArgsConstructor +public class PayloadResolver { + + private ObjectMapper mapper; + + public BidRequest enrichBidRequest(BidRequest bidRequest, TargetingResult targetingResults) { + if (bidRequest == null) { + return null; + } + if (targetingResults == null) { + return bidRequest; + } + + final User user = getUser(targetingResults); + if (user == null) { + return bidRequest; + } + + return BidRequestBuilder.of(bidRequest) + .addEids(user.getEids()) + .addData(user.getData()) + .build(); + } + + public BidResponse enrichBidResponse(BidResponse bidResponse, List targeting) { + if (bidResponse == null) { + return null; + } + if (CollectionUtils.isEmpty(targeting)) { + return bidResponse; + } + + return BidResponseBuilder.of(bidResponse, mapper) + .applyTargeting(targeting) + .build(); + } + + public BidRequest clearBidRequest(BidRequest bidRequest) { + if (bidRequest == null) { + return null; + } + + return BidRequestBuilder.of(bidRequest) + .clearExtUserOptable() + .build(); + } + + private User getUser(TargetingResult targetingResults) { + return Optional.ofNullable(targetingResults) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .orElse(null); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java new file mode 100644 index 00000000000..ab71876f640 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -0,0 +1,83 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.codehaus.plexus.util.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +@AllArgsConstructor +public class QueryBuilder { + + private String idPrefixOrder; + + public String build(List ids, OptableAttributes optableAttributes) { + if (CollectionUtils.isEmpty(ids)) { + return null; + } + + final StringBuilder sb = new StringBuilder(); + final List reorderedIds = reorderIds(ids); + if (CollectionUtils.isNotEmpty(reorderedIds)) { + buildQueryString(sb, reorderedIds); + } + addAttributes(sb, optableAttributes); + + return sb.toString(); + } + + private List reorderIds(List ids) { + if (!StringUtils.isEmpty(idPrefixOrder)) { + final int lastIndex = ids.size() - 1; + final List order = Stream.of(idPrefixOrder.split(",", -1)).toList(); + final List orderedIds = new ArrayList<>(ids); + orderedIds.sort(Comparator.comparing(item -> { + int value = order.indexOf(item.getName()); + if (value == -1) { + value = lastIndex; + } + return value; + })); + + return orderedIds; + } + return ids; + } + + private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes) { + Optional.ofNullable(optableAttributes.getReg()).ifPresent(reg -> sb.append("®=").append(reg)); + Optional.ofNullable(optableAttributes.getTcf()).ifPresent(tcf -> sb.append("&tcf=").append(tcf)); + Optional.ofNullable(optableAttributes.getGpp()).ifPresent(tcf -> sb.append("&gpp=").append(tcf)); + Optional.ofNullable(optableAttributes.getGppSid()).ifPresent(gppSids -> { + if (CollectionUtils.isNotEmpty(gppSids)) { + sb.append("&gpp_sid=").append(gppSids.stream().findFirst()); + } + }); + Optional.ofNullable(optableAttributes.getTimeout()).ifPresent(timeout -> { + sb.append("&timeout=").append(timeout).append("ms"); + }); + } + + private void buildQueryString(StringBuilder sb, List ids) { + final int size = ids.size(); + IntStream.range(0, size) + .forEach(index -> { + final Id id = ids.get(index); + sb.append(URLEncoder.encode( + "%s:%s".formatted(id.getName(), id.getValue()), + StandardCharsets.UTF_8)); + if (index != size - 1) { + sb.append("&id="); + } + }); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java new file mode 100644 index 00000000000..929ae8fe847 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java @@ -0,0 +1,20 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Optional; + +public class BaseMerger { + + protected ObjectNode mergeExt(ObjectNode origin, ObjectNode newExt) { + if (newExt == null) { + return origin; + } + + return Optional.ofNullable(origin) + .map(it -> { + newExt.fieldNames().forEachRemaining(field -> it.set(field, newExt.get(field))); + return it; + }).orElse(newExt); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java new file mode 100644 index 00000000000..6280afbcff5 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java @@ -0,0 +1,101 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.User; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@AllArgsConstructor(staticName = "of") +public class BidRequestBuilder { + + BidRequest bidRequest; + + private static final EidsMerger EIDS_MERGER = new EidsMerger(); + + private static final DataMerger DATA_MERGER = new DataMerger(); + + private static final PayloadCleaner PAYLOAD_CLEANER = new PayloadCleaner(); + + public BidRequestBuilder addEids(List eids) { + if (bidRequest == null || CollectionUtils.isEmpty(eids)) { + return this; + } + + final User user = getOrCreateUser(bidRequest); + bidRequest = bidRequest.toBuilder() + .user(user.toBuilder() + .eids(EIDS_MERGER.merge(user.getEids(), eids)) + .build()) + .build(); + + return this; + } + + public BidRequestBuilder addData(List data) { + if (bidRequest == null || CollectionUtils.isEmpty(data)) { + return this; + } + + final User user = getOrCreateUser(bidRequest); + + bidRequest = bidRequest.toBuilder() + .user(user.toBuilder() + .data(DATA_MERGER.merge(user.getData(), data)) + .build()) + .build(); + + return this; + } + + private static Optional getUserOpt(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getUser); + } + + private static User getOrCreateUser(BidRequest bidRequest) { + return getUserOpt(bidRequest) + .orElseGet(() -> User.builder().eids(new ArrayList<>()).build()); + } + + public BidRequest build() { + return bidRequest; + } + + public BidRequestBuilder clearExtUserOptable() { + if (bidRequest == null) { + return this; + } + + final Optional userOpt = getUserOpt(bidRequest); + + final JsonNode userExtOptable = userOpt.map(User::getExt) + .map(it -> it.getProperty("optable")) + .map(it -> PAYLOAD_CLEANER.cleanUserExtOptable((ObjectNode) it)) + .orElse(null); + + if (userExtOptable != null) { + final ExtUser extUser = userOpt.map(User::getExt).orElse(null); + if (!userExtOptable.isEmpty()) { + extUser.addProperty("optable", userExtOptable); + } else { + extUser.addProperty("optable", null); + } + bidRequest = bidRequest.toBuilder() + .user(userOpt.get().toBuilder() + .ext(extUser) + .build()) + .build(); + } + + return this; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java new file mode 100644 index 00000000000..abf8c2186f7 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java @@ -0,0 +1,84 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +@AllArgsConstructor(staticName = "of") +public class BidResponseBuilder { + + private static final BaseMerger EXT_MERGER = new BaseMerger(); + + private BidResponse bidResponse; + + private ObjectMapper mapper; + + public BidResponseBuilder applyTargeting(List targeting) { + if (CollectionUtils.isEmpty(targeting)) { + return this; + } + + final ObjectNode node = targetingToObjectNode(targeting); + if (node == null) { + return this; + } + + final List seatBids = Optional.ofNullable(bidResponse.getSeatbid()) + .orElse(Collections.emptyList()) + .stream().map(seatBid -> { + final List bids = Optional.ofNullable(seatBid.getBid()).orElse(Collections.emptyList()) + .stream().map(bid -> { + final ObjectNode extNode = Optional.ofNullable(bid.getExt()) + .orElseGet(() -> mapper.createObjectNode()); + final ObjectNode prebidNode = Optional.ofNullable((ObjectNode) (extNode.get("prebid"))) + .orElseGet(() -> mapper.createObjectNode()); + final ObjectNode targetingNode = + Optional.ofNullable((ObjectNode) (prebidNode.get("targeting"))) + .orElseGet(() -> mapper.createObjectNode()); + final JsonNode mergedTargetingNode = EXT_MERGER.mergeExt(targetingNode, node); + + prebidNode.set("targeting", mergedTargetingNode); + extNode.set("prebid", prebidNode); + + return bid.toBuilder().ext(extNode).build(); + }).toList(); + + return seatBid.toBuilder().bid(bids).build(); + }).toList(); + + bidResponse = bidResponse.toBuilder() + .seatbid(seatBids) + .build(); + + return this; + } + + private ObjectNode targetingToObjectNode(List targeting) { + final ObjectNode node = mapper.createObjectNode(); + targeting.forEach(it -> { + final List ids = it.getIds(); + if (CollectionUtils.isNotEmpty(ids)) { + final List strIds = ids.stream().map(AudienceId::getId).toList(); + node.putIfAbsent(it.getKeyspace(), TextNode.valueOf(String.join(",", strIds))); + } + }); + + return node; + } + + public BidResponse build() { + return bidResponse; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java new file mode 100644 index 00000000000..57452f3b3f2 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java @@ -0,0 +1,101 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.iab.openrtb.request.Segment; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class DataMerger extends BaseMerger { + + public List merge(List destination, List source) { + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map idToData = mapDataToId(destination); + if (idToData == null || idToData.isEmpty()) { + return source.stream().map(this::toData).toList(); + } + + source.forEach(data -> idToData.compute(data.getId(), (id, item) -> item != null + ? mergeData(item, data) + : toData(data))); + + return idToData.values().stream().toList(); + } + + private com.iab.openrtb.request.Data mergeData(com.iab.openrtb.request.Data destination, Data source) { + if (source == null) { + return destination; + } + + final Map idToSegment = mapSegmentToId(destination.getSegment()); + if (idToSegment == null) { + return toData(source); + } + + Optional.ofNullable(source.getSegment()).ifPresent(it -> + it.forEach(seg -> idToSegment.compute(seg.getId(), (id, item) -> item != null + ? mergeSegment(item, seg) + : toSegment(seg)))); + + return destination.toBuilder() + .segment(idToSegment.values().stream().toList()) + .build(); + } + + private Segment mergeSegment(Segment destination, + org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment source) { + + return Segment.builder() + .id(destination.getId()) + .value(destination.getValue()) + .name(destination.getName()) + .ext(mergeExt(destination.getExt(), source.getExt())) + .build(); + } + + private Segment toSegment(org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment segment) { + return Segment.builder() + .id(segment.getId()) + .ext(segment.getExt()) + .build(); + } + + private Map mapSegmentToId(List segment) { + return CollectionUtils.isNotEmpty(segment) + ? segment.stream().collect(Collectors.toMap(Segment::getId, it -> it)) + : null; + } + + private com.iab.openrtb.request.Data toData(Data data) { + if (data == null) { + return null; + } + + final List segment = Optional.of(data) + .map(Data::getSegment) + .map(it -> it.stream() + .map(seg -> Segment.builder() + .id(seg.getId()) + .ext(seg.getExt()) + .build()) + .toList()) + .orElse(null); + + return com.iab.openrtb.request.Data.builder() + .id(data.getId()) + .segment(segment) + .build(); + } + + private Map mapDataToId(List data) { + return CollectionUtils.isNotEmpty(data) + ? data.stream().collect(Collectors.toMap(com.iab.openrtb.request.Data::getId, it -> it)) + : null; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java new file mode 100644 index 00000000000..805635c469c --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java @@ -0,0 +1,71 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import org.apache.commons.collections4.CollectionUtils; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class EidsMerger extends BaseMerger { + + public List merge(List destination, List source) { + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map sourceToEid = mapEidToSource(destination); + + if (sourceToEid == null || sourceToEid.isEmpty()) { + return source; + } + + source.forEach(eid -> sourceToEid.compute(eid.getSource(), (id, item) -> item != null + ? mergeEids(item, eid) + : eid)); + + return sourceToEid.values().stream().toList(); + } + + private Eid mergeEids(Eid destination, Eid source) { + if (source == null) { + return destination; + } + + final Map idToUid = mapUidToId(destination.getUids()); + + if (idToUid == null || idToUid.isEmpty()) { + return source; + } + + Optional.ofNullable(source.getUids()) + .ifPresent(it -> it.forEach(uid -> idToUid.compute(uid.getId(), (id, item) -> item != null + ? mergeUids(item, uid) + : uid))); + + return destination.toBuilder() + .uids(idToUid.values().stream().toList()) + .build(); + } + + private Uid mergeUids(Uid destination, Uid source) { + return destination.toBuilder() + .atype(source.getAtype()) + .ext(mergeExt(destination.getExt(), source.getExt())) + .build(); + } + + private Map mapEidToSource(List eids) { + return CollectionUtils.isNotEmpty(eids) + ? eids.stream().collect(Collectors.toMap(Eid::getSource, eid -> eid)) + : null; + } + + private Map mapUidToId(List uids) { + return CollectionUtils.isNotEmpty(uids) + ? uids.stream().collect(Collectors.toMap(Uid::getId, uid -> uid)) + : null; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java new file mode 100644 index 00000000000..4a0e3605665 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java @@ -0,0 +1,14 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.List; + +public class PayloadCleaner { + + private static final List FIELDS_FILTER = List.of("email", "phone", "zip", "vid"); + + public ObjectNode cleanUserExtOptable(ObjectNode optable) { + return optable.deepCopy().remove(FIELDS_FILTER); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java new file mode 100644 index 00000000000..703af7399f1 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -0,0 +1,160 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.netty.channel.ConnectTimeoutException; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.impl.headers.HeadersMultiMap; +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; +import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; +import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableError; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeoutException; + +@AllArgsConstructor +public class APIClient { + + private static final Logger logger = LoggerFactory.getLogger(APIClient.class); + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private final String endpoint; + + private final HttpClient httpClient; + + private final double logSamplingRate; + + private final OptableResponseParser responseParser; + + private final String apiKey; + + public Future getTargeting(String query, List ips, long timeout) { + logger.debug("Query string: %s".formatted(query)); + + final MultiMap headers = HeadersMultiMap.headers() + .add(HttpUtil.ACCEPT_HEADER, "application/json"); + + if (StringUtils.isNotEmpty(apiKey)) { + headers.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer %s".formatted(apiKey)); + } + + if (CollectionUtils.isNotEmpty(ips)) { + ips.forEach(ip -> { + headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip); + }); + } + + final HttpRequest request = HttpRequest.builder() + .uri(endpoint) + .query(query) + .headers(headers) + .build(); + + return doRequest(request, timeout); + } + + private Future doRequest(HttpRequest httpRequest, long timeout) { + return createRequest(httpRequest, timeout) + .compose(response -> processResponse(response, httpRequest)) + .recover(exception -> failResponse(exception, httpRequest)) + .map(this::validateResponse) + .map(it -> parseSilent(it, httpRequest)) + .recover(exception -> logParsingError(exception, httpRequest)); + } + + private TargetingResult parseSilent(OptableCall optableCall, HttpRequest httpRequest) { + try { + return responseParser.parse(optableCall); + } catch (Exception e) { + logParsingError(e, httpRequest); + } + return null; + } + + private Future logParsingError(Throwable exception, HttpRequest httpRequest) { + logger.warn("Error occurred while parsing HTTP response from the Optable url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); + logger.debug("Error occurred while parsing HTTP response from the Optable url: {}", + exception, httpRequest.getUri()); + + return null; + } + + private Future createRequest(HttpRequest httpRequest, long remainingTimeout) { + try { + return httpClient.request(HttpMethod.GET, + httpRequest.getUri() + "?id=" + httpRequest.getQuery(), + httpRequest.getHeaders(), + (String) null, + remainingTimeout); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + private static Future processResponse(HttpClientResponse response, + HttpRequest httpRequest) { + + final int statusCode = response.getStatusCode(); + final HttpResponse httpResponse = HttpResponse.of(statusCode, response.getHeaders(), response.getBody()); + return Future.succeededFuture(OptableCall.succeededHttp(httpRequest, httpResponse, errorOrNull(statusCode))); + } + + private static OptableError errorOrNull(int statusCode) { + if (statusCode != HttpResponseStatus.OK.code() && statusCode != HttpResponseStatus.NO_CONTENT.code()) { + return OptableError.of( + "Unexpected status code: %s.".formatted(statusCode), + statusCode == HttpResponseStatus.BAD_REQUEST.code() + ? OptableError.Type.BAD_INPUT + : OptableError.Type.BAD_SERVER_RESPONSE); + } + return null; + } + + private Future failResponse(Throwable exception, HttpRequest httpRequest) { + conditionalLogger.warn("Error occurred while sending HTTP request to the Optable url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); + logger.debug("Error occurred while sending HTTP request to the Optable url: {}", + exception, httpRequest.getUri()); + + final OptableError.Type errorType = + exception instanceof TimeoutException || exception instanceof ConnectTimeoutException + ? OptableError.Type.TIMEOUT + : OptableError.Type.GENERIC; + + return Future.succeededFuture( + OptableCall.failedHttp(httpRequest, OptableError.of(exception.getMessage(), errorType))); + } + + private OptableCall validateResponse(OptableCall response) { + return Optional.ofNullable(response) + .map(OptableCall::getResponse) + .map(resp -> { + if (resp.getStatusCode() != HttpResponseStatus.OK.code()) { + conditionalLogger.warn(("Error occurred while sending HTTP request to the " + + "Optable url: %s with message: %s").formatted(response.getRequest().getUri(), + resp.getBody()), logSamplingRate); + logger.debug("Error occurred while sending HTTP request to the Optable url: {}"); + + return null; + } + + return response; + }) + .orElse(null); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java new file mode 100644 index 00000000000..8da7722c2aa --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.net.JksOptions; +import org.prebid.server.spring.config.model.HttpClientProperties; +import org.prebid.server.vertx.httpclient.BasicHttpClient; +import org.prebid.server.vertx.httpclient.HttpClient; + +import java.util.concurrent.TimeUnit; + +public class OptableHttpClientWrapper { + + private HttpClient httpClient; + + public OptableHttpClientWrapper(Vertx vertx, HttpClientProperties httpClientProperties) { + this.httpClient = createBasicHttpClient(vertx, httpClientProperties); + } + + public HttpClient getHttpClient() { + return httpClient; + } + + private static HttpClient createBasicHttpClient(Vertx vertx, HttpClientProperties httpClientProperties) { + final HttpClientOptions options = new HttpClientOptions() + .setMaxPoolSize(httpClientProperties.getMaxPoolSize()) + .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) + .setIdleTimeout(httpClientProperties.getIdleTimeoutMs()) + .setPoolCleanerPeriod(httpClientProperties.getPoolCleanerPeriodMs()) + .setTryUseCompression(httpClientProperties.getUseCompression()) + .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) + // Vert.x's HttpClientRequest needs this value to be 2 for redirections to be followed once, + // 3 for twice, and so on + .setMaxRedirects(httpClientProperties.getMaxRedirects() + 1); + + if (httpClientProperties.getSsl()) { + final JksOptions jksOptions = new JksOptions() + .setPath(httpClientProperties.getJksPath()) + .setPassword(httpClientProperties.getJksPassword()); + + options + .setSsl(true) + .setKeyStoreOptions(jksOptions); + } + + return new BasicHttpClient(vertx, vertx.createHttpClient(options)); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java new file mode 100644 index 00000000000..e9519cfcb91 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import lombok.AllArgsConstructor; +import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.JacksonMapper; + +import java.util.Optional; + +@AllArgsConstructor +public class OptableResponseParser { + + private final JacksonMapper mapper; + + public TargetingResult parse(OptableCall call) { + return Optional.ofNullable(call) + .map(OptableCall::getResponse) + .map(resp -> mapper.decodeValue(resp.getBody(), TargetingResult.class)) + .orElse(null); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java new file mode 100644 index 00000000000..1ef1639e63d --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -0,0 +1,170 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import io.vertx.core.MultiMap; +import io.vertx.core.http.impl.headers.HeadersMultiMap; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpStatus; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.model.TimeoutContext; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; + +public abstract class BaseOptableTest { + + private final ObjectMapper mapper = new ObjectMapper(); + + protected ModuleContext givenModuleContext() { + return givenModuleContext(null); + } + + protected ModuleContext givenModuleContext(List audiences) { + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setMetrics(Metrics.builder() + .moduleStartTime(System.currentTimeMillis()) + .build()); + moduleContext.setTargeting(audiences); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + + return moduleContext; + } + + protected AuctionContext givenAuctionContext(Timeout timeout) { + return AuctionContext.builder() + .bidRequest(givenBidRequest()) + .timeoutContext(TimeoutContext.of(0, timeout, 1)) + .build(); + } + + protected BidRequest givenBidRequest() { + return BidRequest.builder() + .user(givenUser()) + .device(givenDevice()) + .cur(List.of("USD")) + .build(); + } + + protected BidResponse givenBidResponse() { + final ObjectNode targetingNode = mapper.createObjectNode(); + targetingNode.set("attribute1", TextNode.valueOf("value1")); + targetingNode.set("attribute2", TextNode.valueOf("value1")); + final ObjectNode bidderNode = mapper.createObjectNode(); + bidderNode.set("targeting", targetingNode); + final ObjectNode bidExtNode = mapper.createObjectNode(); + bidExtNode.set("prebid", bidderNode); + + return BidResponse.builder() + .seatbid(List.of(SeatBid.builder() + .bid(List.of(Bid.builder() + .ext(bidExtNode) + .build())) + .build())) + .build(); + } + + protected TargetingResult givenTargetingResult() { + return new TargetingResult( + List.of(new Audience( + "provider", + List.of(new AudienceId("id")), + "keyspace", + 1 + )), + new Ortb2( + new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User( + List.of(Eid.builder() + .source("source") + .uids(List.of(Uid.builder() + .id("id") + .build())) + .build()), + List.of(new Data("id", List.of(new Segment("id", null)))) + ) + ) + ); + } + + protected TargetingResult givenEmptyTargetingResult() { + return new TargetingResult(Collections.emptyList(), new Ortb2(null)); + } + + protected User givenUser() { + final ObjectNode optable = mapper.createObjectNode(); + optable.set("email", TextNode.valueOf("email")); + optable.set("phone", TextNode.valueOf("phone")); + optable.set("zip", TextNode.valueOf("zip")); + optable.set("vid", TextNode.valueOf("vid")); + + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", optable); + + return User.builder() + .geo(Geo.builder().country("country-u").region("region-u").build()) + .ext(extUser) + .build(); + } + + protected Device givenDevice() { + return Device.builder().geo(Geo.builder().country("country-d").region("region-d").build()).build(); + } + + protected HttpClientResponse givenSuccessHttpResponse(String fileName) { + final MultiMap headers = HeadersMultiMap.headers().add("Content-Type", "application/json"); + return HttpClientResponse.of(HttpStatus.SC_OK, headers, givenBodyFromFile(fileName)); + } + + protected HttpClientResponse givenFailHttpResponse(String fileName) { + return givenFailHttpResponse(HttpStatus.SC_BAD_REQUEST, fileName); + } + + protected HttpClientResponse givenFailHttpResponse(int statusCode, String fileName) { + return HttpClientResponse.of(statusCode, null, givenBodyFromFile(fileName)); + } + + protected String givenBodyFromFile(String fileName) { + InputStream inputStream = null; + try { + inputStream = Files.newInputStream(Paths.get("src/test/resources/" + fileName)); + return IOUtils.toString(inputStream, StandardCharsets.UTF_8); + } catch (IOException e) { + return null; + } finally { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + // ignore + } + } + } + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java new file mode 100644 index 00000000000..a521090165b --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -0,0 +1,140 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.response.BidResponse; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionResponseHook; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { + + @Mock + AuctionResponsePayload auctionResponsePayload; + @Mock + AuctionInvocationContext invocationContext; + + private PayloadResolver payloadResolver; + + private AuctionResponseValidator auctionResponseValidator; + + private final ObjectMapper mapper = new ObjectMapper(); + + private AuctionResponseHook target; + + @BeforeEach + public void setUp() { + payloadResolver = new PayloadResolver(mapper); + auctionResponseValidator = new AuctionResponseValidator(); + target = new OptableTargetingAuctionResponseHook(new AnalyticTagsResolver(mapper), + payloadResolver, true, auctionResponseValidator); + } + + @Test + public void shouldHaveCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-auction-response-hook"); + + } + + @Test + public void shouldReturnResultWithNoActionAndPBSAnalyticsTags() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); + + // when + final Future> future = target.call(auctionResponsePayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.analyticsTags().activities().getFirst() + .results().getFirst().values().get("execution-time")).isNotNull(); + assertThat(result.errors()).isNull(); + } + + @Test + public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", + List.of(new AudienceId("audienceId")), "keyspace", 1)))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = target.call(auctionResponsePayload, + invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + + assertThat(targeting) + .isNotNull() + .hasSize(3); + + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); + } + + @Test + public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", + List.of(new AudienceId("audienceId")), "keyspace", 1)))); + target = new OptableTargetingAuctionResponseHook(new AnalyticTagsResolver(mapper), + payloadResolver, false, auctionResponseValidator); + + // when + final Future> future = target.call(auctionResponsePayload, + invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java new file mode 100644 index 00000000000..f621b1ee7c2 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -0,0 +1,41 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.v1.Hook; +import org.prebid.server.hooks.v1.InvocationContext; +import org.prebid.server.hooks.v1.Module; + +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class OptableTargetingModuleTest { + + @Test + public void shouldReturnNonBlankCode() { + // given + final Module module = new OptableTargetingModule(null); + + // when and then + assertThat(module.code()) + .isNotBlank() + .isEqualTo("optable-targeting"); + + } + + @Test + public void shouldReturnHooks() { + // given + final Collection> hooks = + List.of(new OptableTargetingProcessedAuctionRequestHook(null, null, null, null), + new OptableTargetingAuctionResponseHook(null, null, true, null)); + + final Module module = new OptableTargetingModule(hooks); + + // when and then + assertThat(module.hooks()) + .hasSize(2) + .isEqualTo(hooks); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java new file mode 100644 index 00000000000..0e79998bd95 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -0,0 +1,174 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.IpResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { + + @Mock + OptableTargetingProperties optableTargetingProperties; + @Mock + OptableTargeting optableTargeting; + @Mock + AuctionRequestPayload auctionRequestPayload; + @Mock + AuctionInvocationContext invocationContext; + @Mock + Timeout timeout; + + private ObjectMapper mapper; + + private PayloadResolver payloadResolver; + + private OptableAttributesResolver optableAttributesResolver; + + private OptableTargetingProcessedAuctionRequestHook target; + + private IpResolver ipResolver; + + @BeforeEach + public void setUp() { + when(timeout.remaining()).thenReturn(1000L); + when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(timeout)); + payloadResolver = new PayloadResolver(mapper); + ipResolver = new IpResolver(); + optableAttributesResolver = new OptableAttributesResolver(ipResolver); + target = new OptableTargetingProcessedAuctionRequestHook(optableTargetingProperties, optableTargeting, + payloadResolver, optableAttributesResolver); + } + + @Test + public void shouldHaveRightCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-processed-auction-request-hook"); + } + + @Test + public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + final BidRequest bidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); + assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + + } + + @Test + public void shouldReturnResultWithCleanedUpUserExtOptableTag() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + final ObjectNode optable = (ObjectNode) result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest() + .getUser().getExt().getProperty("optable"); + + assertThat(optable).isNull(); + } + + @Test + public void shouldReturnResultWithoutUpdateActionWhenBidRequestIsNull() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(null); + when(optableTargeting.getTargeting(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.errors()).isNull(); + } + + @Test + public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), anyLong())).thenReturn(Future.succeededFuture(null)); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java new file mode 100644 index 00000000000..03049834fc6 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java @@ -0,0 +1,77 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.response.Bid; +import com.iab.openrtb.response.BidResponse; +import com.iab.openrtb.response.SeatBid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AuctionResponseValidatorTest { + + private AuctionResponseValidator target; + + @BeforeEach + public void setUp() { + target = new AuctionResponseValidator(); + } + + @Test + public void shouldReturnNobidStatusWhenBidResponseIsEmpty() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + + // when + final EnrichmentStatus result = target.checkEnrichmentPossibility(bidResponse, givenTargeting()); + + // then + assertThat(result).isNotNull() + .returns(Status.FAIL, EnrichmentStatus::status) + .returns(Reason.NOBID, EnrichmentStatus::reason); + } + + @Test + public void shouldReturnNoKeywordsStatusWhenTargetingHasNoIds() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + + // when + final EnrichmentStatus result = target.checkEnrichmentPossibility(bidResponse, givenTargeting()); + + // then + assertThat(result).isNotNull() + .returns(Status.FAIL, EnrichmentStatus::status) + .returns(Reason.NOBID, EnrichmentStatus::reason); + } + + @Test + public void shouldReturnSuccessStatus() { + // given + final BidResponse bidResponse = BidResponse.builder() + .seatbid(List.of(SeatBid.builder() + .bid(List.of(Bid.builder().build())) + .build())) + .build(); + + // when + final EnrichmentStatus result = target.checkEnrichmentPossibility(bidResponse, givenTargeting()); + + // then + assertThat(result).isNotNull() + .returns(Status.SUCCESS, EnrichmentStatus::status) + .returns(Reason.NONE, EnrichmentStatus::reason); + } + + protected List givenTargeting() { + return List.of(new Audience("provider", List.of(new AudienceId("id")), + "keyspace", 1)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java new file mode 100644 index 00000000000..c83fd1f2983 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java @@ -0,0 +1,127 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Imp; +import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.List; +import java.util.Map; +import java.util.function.UnaryOperator; + +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; + +public class IdMapperTest { + + private final ObjectMapper objectMapper = new ObjectMapper(); + private IdsMapper target; + + @BeforeEach + public void setUp() { + target = new IdsMapper(objectMapper, Map.of("test.com", "c")); + } + + @Test + public void shouldMapBidRequestToAllPossibleIds() { + //given + final BidRequest bidRequest = givenBidRequest(builder -> { + final Map eids = Map.of("id5-sync.com", "id5_id", + "test.com", "test_id", "utiq.com", "utiq_id"); + + final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); + + builder.device(givenDevice()) + .user(givenUser(userBuilder -> { + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", extUserOptable); + userBuilder.eids(toEids(eids)) + .ext(extUser) + .build(); + + return userBuilder; + })); + + return builder; + }); + + // when + final List ids = target.toIds(bidRequest); + + // then + assertThat(ids).isNotNull() + .contains(Id.of(Id.EMAIL, "email")) + .contains(Id.of(Id.PHONE, "123")) + .contains(Id.of(Id.ZIP, "321")) + .contains(Id.of(Id.OPTABLE_VID, "vid")) + .contains(Id.of(Id.GOOGLE_GAID, "ifa")) + .doesNotContain(Id.of(Id.APPLE_IDFA, "ifa")) + .contains(Id.of(Id.ID5, "id5_id")) + .contains(Id.of(Id.UTIQ, "utiq_id")) + .contains(Id.of("c", "test_id")); + } + + private User givenUser(UnaryOperator userCustomizer) { + return userCustomizer.apply(User.builder()).build(); + } + + private Device givenDevice() { + return Device.builder() + .ip("127.0.0.1") + .ipv6("0:0:0:0:0:0:0:1") + .lmt(0) + .os("android") + .ifa("ifa") + .build(); + } + + @Test + public void shouldMapNothing() { + //given + final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder); + + // when + final List ids = target.toIds(bidRequest); + + // then + assertThat(ids).isNotNull(); + } + + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder() + .id("requestId") + .imp(singletonList(Imp.builder() + .id("impId") + .build()))) + .build(); + } + + private ExtUserOptable givenOptable() { + return ExtUserOptable.builder() + .email("email") + .phone("123") + .zip("321") + .vid("vid") + .build(); + } + + private List toEids(Map eids) { + return eids.entrySet().stream() + .map(it -> Eid.builder() + .source(it.getKey()) + .uids(List.of(Uid.builder() + .id(it.getValue()) + .build())) + .build()) + .toList(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java new file mode 100644 index 00000000000..474596b2dd7 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -0,0 +1,217 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.gpp.encoder.GppModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.gpp.model.GppContext; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.geolocation.model.GeoInfo; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.Privacy; +import org.prebid.server.privacy.model.PrivacyContext; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableAttributesResolverTest extends BaseOptableTest { + + private OptableAttributesResolver target; + + @Mock(strictness = LENIENT) + private TcfContext tcfContext; + + @Mock(strictness = LENIENT) + private GppContext gppContext; + + @Mock(strictness = LENIENT) + private GeoInfo geoInfo; + + @Mock + private OptableTargetingProperties properties; + + @BeforeEach + public void setUp() { + when(properties.getTimeout()).thenReturn(100L); + target = new OptableAttributesResolver(new IpResolver()); + } + + @Test + public void shouldResolveTcfAttributesWhenConsentIsValid() { + // given + when(tcfContext.isConsentValid()).thenReturn(true); + when(tcfContext.getConsentString()).thenReturn("consent"); + final AuctionContext auctionContext = givenAuctionContext(tcfContext); + + // when + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("gdpr", OptableAttributes::getReg) + .returns("consent", OptableAttributes::getTcf); + } + + @Test + public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { + // given + when(tcfContext.isConsentValid()).thenReturn(false); + when(tcfContext.getConsentString()).thenReturn("consent"); + final AuctionContext auctionContext = givenAuctionContext(tcfContext); + + // when + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(null, OptableAttributes::getReg) + .returns(null, OptableAttributes::getTcf) + .returns(List.of("8.8.8.8"), OptableAttributes::getIps); + } + + @Test + public void shouldResolveGppGdprAttributes() { + // given + final GppModel gppModel = mock(); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = givenAuctionContext(gppContext); + + // when + + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("gdpr", OptableAttributes::getReg) + .returns("consent", OptableAttributes::getGpp) + .returns(Set.of(1), OptableAttributes::getGppSid); + } + + @Test + public void shouldResolveGppCanadaAttributes() { + // given + final GppModel gppModel = mock(); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(5))); + final AuctionContext auctionContext = givenAuctionContext(gppContext); + + // when + + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("can", OptableAttributes::getReg) + .returns("consent", OptableAttributes::getGpp) + .returns(Set.of(5), OptableAttributes::getGppSid); + } + + @Test + public void shouldResolveGppUSAttributes() { + // given + final GppModel gppModel = mock(); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(8))); + final AuctionContext auctionContext = givenAuctionContext(gppContext); + + // when + + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("us", OptableAttributes::getReg) + .returns("consent", OptableAttributes::getGpp) + .returns(Set.of(8), OptableAttributes::getGppSid); + } + + @Test + public void shouldResolveGeoInfoUSAttributes() { + // given + when(geoInfo.getCountry()).thenReturn("United States"); + final AuctionContext auctionContext = givenAuctionContext(geoInfo); + + // when + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("us", OptableAttributes::getReg) + .returns(null, OptableAttributes::getGpp) + .returns(null, OptableAttributes::getTcf); + } + + @Test + public void shouldResolveGeoInfoGDPRAttributes() { + // given + when(geoInfo.getCountry()).thenReturn("Malta"); + final AuctionContext auctionContext = givenAuctionContext(geoInfo); + + // when + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("gdpr", OptableAttributes::getReg) + .returns(null, OptableAttributes::getGpp) + .returns(null, OptableAttributes::getTcf); + } + + @Test + public void shouldResolveGeoInfoCanadaAttributes() { + // given + when(geoInfo.getCountry()).thenReturn("Quebec"); + final AuctionContext auctionContext = givenAuctionContext(geoInfo); + + // when + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("can", OptableAttributes::getReg) + .returns(null, OptableAttributes::getGpp) + .returns(null, OptableAttributes::getTcf); + } + + @Test + public void shouldResolveGeoInfoGDPRForRegionAttributes() { + // given + when(geoInfo.getRegion()).thenReturn("Mayotte"); + final AuctionContext auctionContext = givenAuctionContext(geoInfo); + + // when + final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("gdpr", OptableAttributes::getReg) + .returns(null, OptableAttributes::getGpp) + .returns(null, OptableAttributes::getTcf); + } + + public AuctionContext givenAuctionContext(TcfContext tcfContext) { + return AuctionContext.builder() + .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, + "8.8.8.8")).build(); + } + + public AuctionContext givenAuctionContext(GppContext gppContext) { + return AuctionContext.builder().gppContext(gppContext).build(); + } + + public AuctionContext givenAuctionContext(GeoInfo geoInfo) { + return AuctionContext.builder().geoInfo(geoInfo).build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java new file mode 100644 index 00000000000..7a2cfd08921 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -0,0 +1,115 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; + +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableTargetingTest extends BaseOptableTest { + + @Mock + private IdsMapper idsMapper; + + @Mock + private APIClient apiClient; + + private QueryBuilder queryBuilder = new QueryBuilder("c,c1,email"); + + private OptableTargeting target; + + private OptableAttributes optableAttributes; + + @BeforeEach + public void setUp() { + optableAttributes = givenOptableAttributes(); + target = new OptableTargeting(idsMapper, queryBuilder, apiClient); + } + + @Test + public void shouldReturnTargetingResults() { + // given + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + } + + @Test + public void shouldNotFailWhenNoIdsMapped() { + // given + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of()); + verifyNoInteractions(apiClient); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + assertThat(targetingResult.result()).isNull(); + } + + @Test + public void shouldNotFailWhenApiClientIsFailed() { + // given + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), anyLong())).thenReturn(null); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + assertThat(targetingResult.result()).isNull(); + } + + @Test + public void shouldNotFailWhenApiClientReturnsFailFuture() { + // given + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), anyLong())).thenReturn(Future.failedFuture("File")); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + assertThat(targetingResult.result()).isNull(); + } + + private OptableAttributes givenOptableAttributes() { + return OptableAttributes.of("gdpr").toBuilder() + .gpp("gpp") + .gppSid(Set.of(2)) + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java new file mode 100644 index 00000000000..fe9f1ba44b1 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java @@ -0,0 +1,156 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import com.iab.openrtb.response.BidResponse; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PayloadResolverTest extends BaseOptableTest { + + private ObjectMapper mapper = new ObjectMapper(); + + private PayloadResolver target; + + @BeforeEach + public void setUp() { + target = new PayloadResolver(mapper); + } + + @Test + public void shouldReturnOriginBidRequestWhenNoTargetingResults() { + // given + final BidRequest bidRequest = givenBidRequest(); + + // when + final BidRequest result = target.enrichBidRequest(bidRequest, null); + + // then + assertThat(result).isNotNull(); + final User user = result.getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids()).isNull(); + assertThat(user.getData()).isNull(); + } + + @Test + public void shouldNotFailIfBidRequestIsNull() { + // given + final BidRequest bidRequest = null; + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final BidRequest result = target.enrichBidRequest(bidRequest, targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { + // given + final BidRequest bidRequest = givenBidRequest(); + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final BidRequest result = target.enrichBidRequest(bidRequest, targetingResult); + + // then + assertThat(result).isNotNull(); + final User user = result.getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); + assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + } + + @Test + public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequest(); + final TargetingResult targetingResult = givenEmptyTargetingResult(); + + // when + final BidRequest result = target.enrichBidRequest(bidRequest, targetingResult); + + // then + assertThat(result).isNotNull(); + final User user = result.getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids()).isNull(); + assertThat(user.getData()).isNull(); + } + + @Test + public void shouldEnrichBidResponseByTargetingKeywords() { + // given + final BidResponse bidResponse = givenBidResponse(); + + // when + final BidResponse result = target.enrichBidResponse(bidResponse, givenTargeting()); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNotNull(); + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId,audienceId2"); + } + + @Test + public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { + // given + final BidResponse bidResponse = givenBidResponse(); + + // when + final BidResponse result = target.enrichBidResponse(bidResponse, null); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNotNull(); + assertThat(targeting.get("keyspace")).isNull(); + } + + @Test + public void shouldNotFailWhenResponseIsNull() { + // given + final BidResponse bidResponse = givenBidResponse(); + + // when + final BidResponse result = target.enrichBidResponse(null, givenTargeting()); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNull(); + } + + private List givenTargeting() { + return List.of(new Audience("provider", + List.of(new AudienceId("audienceId"), new AudienceId("audienceId2")), "keyspace", 1)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java new file mode 100644 index 00000000000..d7568d67307 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -0,0 +1,80 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class QueryBuilderTest { + + private QueryBuilder target; + + private OptableAttributes optableAttributes; + + @BeforeEach + public void setUp() { + optableAttributes = givenOptableAttributes(); + target = new QueryBuilder("c,c1"); + } + + @Test + public void shouldBuildQueryStringWhenHaveIds() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final String query = target.build(ids, optableAttributes); + + // then + assertThat(query).contains("e%3Aemail", "p%3A123"); + } + + @Test + public void shouldBuildQueryStringWithExtraAttributes() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final String query = target.build(ids, optableAttributes); + + // then + assertThat(query).contains("®=gdpr", "&tcf=tcf", "&timeout=100ms"); + } + + @Test + public void shouldBuildQueryStringWithRightOrder() { + // given + final List ids = List.of(Id.of(Id.ID5, "ID5"), Id.of(Id.EMAIL, "email"), Id.of("c1", "123"), + Id.of("c", "234")); + + // when + final String query = target.build(ids, optableAttributes); + + // then + assertThat(query).startsWith("c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); + } + + @Test + public void shouldNotBuildQueryStringWhenIdsListIsEmpty() { + // given + final List ids = List.of(); + + // when + final String query = target.build(ids, optableAttributes); + + // then + + assertThat(query).isNull(); + } + + private OptableAttributes givenOptableAttributes() { + return OptableAttributes.of("gdpr").toBuilder() + .timeout(100L) + .tcf("tcf") + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java new file mode 100644 index 00000000000..e7df549391b --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java @@ -0,0 +1,18 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; + +import java.util.Map; + +public class BaseMergerTest { + + protected final ObjectMapper mapper = new ObjectMapper(); + + protected ObjectNode givenExt(Map fields) { + final ObjectNode ext = mapper.createObjectNode(); + fields.forEach(ext::put); + + return ext; + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java new file mode 100644 index 00000000000..d2b7937b394 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java @@ -0,0 +1,151 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.from; + +public class DataMergerTest extends BaseMergerTest { + + private DataMerger target; + + @BeforeEach + public void setUp() { + target = new DataMerger(); + } + + @Test + public void shouldMergeDifferentData() { + // given + final List destination = givenORTBData("dataId1", "segmentId1", + "field1", "value1"); + final List source = givenOptableData("dataId2", "segmentId2", "field2", "value2"); + + // when + final List result = target.merge(destination, source); + + // then + assertThat(result).isNotNull() + .hasSize(2); + + assertThat(result.getFirst()) + .returns("dataId2", from(com.iab.openrtb.request.Data::getId)) + .returns("segmentId2", it -> it.getSegment().getFirst().getId()) + .returns("value2", it -> it.getSegment().getFirst().getExt().get("field2").asText()); + + assertThat(result.get(1)) + .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("segmentId1", it -> it.getSegment().getFirst().getId()) + .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); + } + + @Test + public void shouldMergeSegmentsWithinTheSameData() { + // given + final List destination = givenORTBData("dataId1", "segmentId1", + "field1", "value1"); + final List source = givenOptableData("dataId1", "segmentId2", "field2", "value2"); + + // when + final List result = target.merge(destination, source); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("segmentId1", it -> it.getSegment().getFirst().getId()) + .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()) + .returns("segmentId2", it -> it.getSegment().get(1).getId()) + .returns("value2", it -> it.getSegment().get(1).getExt().get("field2").asText()); + } + + @Test + public void shouldMergeExtWithinTheSameSegment() { + // given + final List destination = givenORTBData("dataId1", "segmentId1", + "field1", "value1"); + final List source = givenOptableData("dataId1", "segmentId1", "field2", "value2"); + + // when + final List result = target.merge(destination, source); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("segmentId1", it -> it.getSegment().getFirst().getId()) + .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()) + .returns("value2", it -> it.getSegment().getFirst().getExt().get("field2").asText()); + } + + @Test + public void shouldUseFirstArgumentWhenSecondIsAbsent() { + // given + final List source = givenOptableData("dataId1", "segmentId1", "field1", "value1"); + + // when + final List result = target.merge(null, source); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("segmentId1", it -> it.getSegment().getFirst().getId()) + .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); + } + + @Test + public void shouldUseSecondArgumentWhenFirstIsAbsent() { + // given + final List destination = givenORTBData("dataId1", "segmentId1", + "field1", "value1"); + // when + final List result = target.merge(destination, null); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("segmentId1", it -> it.getSegment().getFirst().getId()) + .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); + } + + @Test + public void shouldNotFailWhenArgumentsAreAbsent() { + // given and when + final List result = target.merge(null, null); + + // then + assertThat(result).isNull(); + } + + private List givenOptableData(String id, String segmentId, String extField, String extValue) { + return List.of(new Data(id, List.of(new Segment(segmentId, givenExt(Map.of(extField, extValue)))))); + } + + private List givenORTBData(String id, String segmentId, String extField, + String extValue) { + + return List.of(com.iab.openrtb.request.Data.builder() + .id(id) + .segment(List.of(com.iab.openrtb.request.Segment.builder() + .id(segmentId) + .ext(givenExt(Map.of(extField, extValue))) + .build())) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java new file mode 100644 index 00000000000..1396cdbf7b1 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java @@ -0,0 +1,133 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.from; + +public class EidsMergerTest extends BaseMergerTest { + + private EidsMerger target; + + @BeforeEach + public void setUp() { + target = new EidsMerger(); + } + + @Test + public void shouldMergeDifferentEids() { + // given + final List destination = givenEids("source1", "uid1", "field1", "value1"); + final List source = givenEids("source2", "uid2", "field2", "value2"); + + // when + final List result = target.merge(destination, source); + + // then + assertThat(result).isNotNull() + .hasSize(2); + + assertThat(result.getFirst()) + .returns("source1", from(Eid::getSource)) + .returns("uid1", it -> it.getUids().getFirst().getId()) + .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); + + assertThat(result.get(1)) + .returns("source2", from(Eid::getSource)) + .returns("uid2", it -> it.getUids().getFirst().getId()) + .returns("value2", it -> it.getUids().getFirst().getExt().get("field2").asText()); + } + + @Test + public void shouldMergeUidsWithinTheSameEid() { + // given + final List destination = givenEids("source1", "uid1", "field1", "value1"); + final List source = givenEids("source1", "uid2", "field2", "value2"); + + // when + final List result = target.merge(destination, source); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("source1", from(Eid::getSource)) + .returns("uid2", it -> it.getUids().getFirst().getId()) + .returns("uid1", it -> it.getUids().get(1).getId()) + .returns("value2", it -> it.getUids().getFirst().getExt().get("field2").asText()) + .returns("value1", it -> it.getUids().get(1).getExt().get("field1").asText()); + } + + @Test + public void shouldMergeExtWithinTheSameUid() { + // given + final List destination = givenEids("source1", "uid1", "field1", "value1"); + final List source = givenEids("source1", "uid1", "field2", "value2"); + + // when + final List result = target.merge(destination, source); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("source1", from(Eid::getSource)) + .returns("uid1", it -> it.getUids().getFirst().getId()) + .returns("value2", it -> it.getUids().getFirst().getExt().get("field2").asText()) + .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); + } + + @Test + public void shouldUserFirstUidsListWhenSecondIsAbsent() { + // given + final List destination = givenEids("source1", "uid1", "field1", "value1"); + + // when + final List result = target.merge(destination, null); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("source1", from(Eid::getSource)) + .returns("uid1", it -> it.getUids().getFirst().getId()) + .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); + } + + @Test + public void shouldUserSecondUidsListWhenSecondIsEmpty() { + // given + final List source = givenEids("source1", "uid1", "field1", "value1"); + + // when + final List result = target.merge(null, source); + + // then + assertThat(result).isNotNull() + .hasSize(1); + + assertThat(result.getFirst()) + .returns("source1", from(Eid::getSource)) + .returns("uid1", it -> it.getUids().getFirst().getId()) + .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); + } + + private List givenEids(String source, String uidId, String extField, String extValue) { + return List.of(Eid.builder() + .source(source) + .uids(List.of(Uid.builder() + .id(uidId) + .ext(givenExt(Map.of(extField, extValue))) + .build())) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java new file mode 100644 index 00000000000..beee6fd7f23 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java @@ -0,0 +1,73 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExtMergerTest extends BaseMergerTest { + + private BaseMerger target; + + @BeforeEach + public void setUp() { + target = new BaseMerger(); + } + + @Test + public void shouldMergeTwoExtObjects() { + // given + final ObjectNode destination = givenExt(Map.of("field1", "value1", "field2", "value2")); + final ObjectNode source = givenExt(Map.of("field3", "value3", "field4", "value4")); + + // when + final ObjectNode result = target.mergeExt(destination, source); + + // then + assertThat(result).isNotNull().hasSize(4); + assertThat(result.get("field1").asText()).isEqualTo("value1"); + assertThat(result.get("field2").asText()).isEqualTo("value2"); + assertThat(result.get("field3").asText()).isEqualTo("value3"); + assertThat(result.get("field4").asText()).isEqualTo("value4"); + } + + @Test + public void shouldUseFirstArgumentWhenSecondIsNull() { + // given + final ObjectNode destination = givenExt(Map.of("field1", "value1", "field2", "value2")); + + // when + final ObjectNode result = target.mergeExt(destination, null); + + // then + assertThat(result).isNotNull().hasSize(2); + assertThat(result.get("field1").asText()).isEqualTo("value1"); + assertThat(result.get("field2").asText()).isEqualTo("value2"); + } + + @Test + public void shouldUseSecondArgumentWhenFirstIsNull() { + // given + final ObjectNode source = givenExt(Map.of("field1", "value1", "field2", "value2")); + + // when + final ObjectNode result = target.mergeExt(null, source); + + // then + assertThat(result).isNotNull().hasSize(2); + assertThat(result.get("field1").asText()).isEqualTo("value1"); + assertThat(result.get("field2").asText()).isEqualTo("value2"); + } + + @Test + public void shouldNotFail() { + // given and when + final ObjectNode result = target.mergeExt(null, null); + + // then + assertThat(result).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java new file mode 100644 index 00000000000..246235d1476 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -0,0 +1,209 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import org.apache.http.HttpStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.vertx.httpclient.HttpClient; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class APIClientTest extends BaseOptableTest { + + private static final Double LOG_SAMPLING_RATE = 100.0; + + private static final String ACCEPT_HEADER = "Accept"; + + private static final String AUTHORIZATION_HEADER = "Authorization"; + + private static final String X_FORWARDED_FOR_HEADER = "X-forwarded-for"; + + @Mock + private HttpClient httpClient; + + private APIClient target; + + private final OptableResponseParser parser = new OptableResponseParser(new JacksonMapper(new ObjectMapper())); + + @BeforeEach + public void setUp() { + target = new APIClient("endpoint", + httpClient, + LOG_SAMPLING_RATE, + parser, + null); + } + + @Test + public void shouldReturnTargetingResult() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + assertThat(result.result()).isNotNull(); + final TargetingResult res = result.result(); + final User user = res.getOrtb2().getUser(); + assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("uid_id1"); + assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("segment_id"); + } + + @Test + public void shouldReturnNullWhenEndpointRespondsWithError() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotFailWhenEndpointRespondsWithWrongData() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotFailWhenHttpClientIsCrashed() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenThrow(new NullPointerException()); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotFailWhenInternalErrorOccurs() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + assertThat(result.result()).isNull(); + } + + @Test + public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { + // given + target = new APIClient("endpoint", + httpClient, + LOG_SAMPLING_RATE, + parser, + "key"); + + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient) + .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + assertThat(headersCaptor.getValue().get(ACCEPT_HEADER)).isEqualTo("application/json"); + assertThat(headersCaptor.getValue().get(AUTHORIZATION_HEADER)).isEqualTo("Bearer key"); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient) + .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + assertThat(headersCaptor.getValue().get(ACCEPT_HEADER)).isEqualTo("application/json"); + assertThat(headersCaptor.getValue().get(AUTHORIZATION_HEADER)).isNull(); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldPassThroughIpAddresses() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting( + "query", + List.of("8.8.8.8", "2001:4860:4860::8888"), + 1000); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient) + .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + assertThat(headersCaptor.getValue().getAll(X_FORWARDED_FOR_HEADER)).contains("8.8.8.8", "2001:4860:4860::8888"); + assertThat(result.result()).isNull(); + } + + @Test + public void shouldNotPassThroughIpAddressWhenNotSpecified() { + // given + when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, + "plain_text_response.json"))); + + // when + final Future result = target.getTargeting("query", null, 1000); + + // then + final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); + verify(httpClient) + .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + assertThat(headersCaptor.getValue().get(X_FORWARDED_FOR_HEADER)).isNull(); + assertThat(result.result()).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java new file mode 100644 index 00000000000..a188697be27 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java @@ -0,0 +1,114 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.Vertx; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpClientOptions; +import io.vertx.core.http.HttpClientRequest; +import io.vertx.core.http.HttpClientResponse; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.RequestOptions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.spring.config.model.HttpClientProperties; +import org.prebid.server.vertx.httpclient.HttpClient; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class OptableHttpClientWrapperTest { + + @Mock + private Vertx vertx; + + @Mock(strictness = LENIENT) + io.vertx.core.http.HttpClient httpClient; + + @Mock(strictness = LENIENT) + private HttpClientRequest httpClientRequest; + + @Mock + private HttpClientResponse httpClientResponse; + + private HttpClientProperties httpClientProperties; + + private OptableHttpClientWrapper target; + + @BeforeEach + public void setUp() { + when(httpClient.request(any())).thenReturn(Future.succeededFuture(httpClientRequest)); + given(httpClientRequest.send()).willReturn(Future.succeededFuture(httpClientResponse)); + given(httpClientRequest.send(any(Buffer.class))).willReturn(Future.succeededFuture(httpClientResponse)); + when(vertx.createHttpClient(any(HttpClientOptions.class))).thenReturn(httpClient); + httpClientProperties = giveHttpClientProperties(); + target = new OptableHttpClientWrapper(vertx, httpClientProperties); + } + + @Test + public void shouldCreateHttpClient() { + // given nad when + final HttpClient httpClient = target.getHttpClient(); + + // then + assertThat(httpClient).isNotNull(); + final ArgumentCaptor httpClientOptionsCaptor = + ArgumentCaptor.forClass(HttpClientOptions.class); + verify(vertx).createHttpClient(httpClientOptionsCaptor.capture()); + + assertThat(httpClientOptionsCaptor.getValue()).satisfies(options -> { + assertThat(options.getMaxPoolSize()).isEqualTo(8); + assertThat(options.isKeepAlive()).isEqualTo(true); + assertThat(options.getKeepAliveTimeout()).isEqualTo(60); + assertThat(options.getHttp2KeepAliveTimeout()).isEqualTo(60); + assertThat(options.isTryUseCompression()).isEqualTo(true); + assertThat(options.isSsl()).isEqualTo(true); + }); + } + + @Test + public void requestShouldPerformHttpRequestWithExpectedParams() { + // given and when + target.getHttpClient().request(HttpMethod.POST, "http://www.example.com", + MultiMap.caseInsensitiveMultiMap(), "body", 500L); + + // then + final ArgumentCaptor requestOptionsArgumentCaptor = + ArgumentCaptor.forClass(RequestOptions.class); + verify(httpClient).request(requestOptionsArgumentCaptor.capture()); + + final RequestOptions expectedRequestOptions = new RequestOptions() + .setFollowRedirects(true) + .setConnectTimeout(500L) + .setMethod(HttpMethod.POST) + .setAbsoluteURI("http://www.example.com") + .setHeaders(MultiMap.caseInsensitiveMultiMap()); + assertThat(requestOptionsArgumentCaptor.getValue().toJson()).isEqualTo(expectedRequestOptions.toJson()); + + verify(httpClientRequest).send(eq(Buffer.buffer("body".getBytes()))); + } + + private HttpClientProperties giveHttpClientProperties() { + final HttpClientProperties properties = new HttpClientProperties(); + properties.setMaxPoolSize(8); + properties.setConnectTimeoutMs(2000); + properties.setPoolCleanerPeriodMs(6000); + properties.setIdleTimeoutMs(200); + properties.setUseCompression(true); + properties.setSsl(true); + properties.setJksPath("/some_path"); + properties.setJksPassword("password"); + properties.setMaxRedirects(2); + return properties; + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java new file mode 100644 index 00000000000..5dfae7ab124 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java @@ -0,0 +1,96 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.json.DecodeException; +import org.prebid.server.json.JacksonMapper; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class OptableResponseParserTest extends BaseOptableTest { + + private OptableResponseParser target; + + private final JacksonMapper mapper = new JacksonMapper(new ObjectMapper()); + + @BeforeEach + public void setUp() { + + target = new OptableResponseParser(mapper); + } + + @Test + public void shouldNotFailWhenSourceIsNull() { + // given and when + final TargetingResult result = target.parse(null); + + //then + assertThat(result).isNull(); + } + + @Test + public void shouldNotFailWhenResponseIsNull() { + // given + final OptableCall optableCall = OptableCall.succeededHttp(null, null, null); + + //when + final TargetingResult result = target.parse(optableCall); + + //then + assertThat(result).isNull(); + } + + @Test + public void shouldNotFailWhenResponseBodyIsWrong() { + // given + final HttpResponse response = givenSuccessResponse("{\"field'\": \"value\"}"); + final OptableCall optableCall = OptableCall.succeededHttp(null, response, null); + + //when + final TargetingResult result = target.parse(optableCall); + + //then + assertThat(result).isNotNull(); + assertThat(result.getOrtb2()).isNull(); + } + + @Test + public void shouldParseRightResponse() { + // given + final HttpResponse response = givenSuccessResponse(givenBodyFromFile("targeting_response.json")); + final OptableCall optableCall = OptableCall.succeededHttp(null, response, null); + + //when + final TargetingResult result = target.parse(optableCall); + + //then + assertThat(result).isNotNull(); + final User user = result.getOrtb2().getUser(); + assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("uid_id1"); + assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("segment_id"); + } + + @Test + public void shouldFailWhenGotNotJsonString() { + // given + final HttpResponse response = givenSuccessResponse("random string"); + final OptableCall optableCall = OptableCall.succeededHttp(null, response, null); + + //when and then + assertThrows( + DecodeException.class, + () -> target.parse(optableCall) + ); + } + + private HttpResponse givenSuccessResponse(String body) { + return HttpResponse.of(200, null, body); + } +} diff --git a/extra/modules/optable-targeting/src/test/resources/error_response.json b/extra/modules/optable-targeting/src/test/resources/error_response.json new file mode 100644 index 00000000000..4250099e7ba --- /dev/null +++ b/extra/modules/optable-targeting/src/test/resources/error_response.json @@ -0,0 +1 @@ +{"details": "Error message"} diff --git a/extra/modules/optable-targeting/src/test/resources/plaint_text_response.json b/extra/modules/optable-targeting/src/test/resources/plaint_text_response.json new file mode 100644 index 00000000000..ec6816d6f25 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/resources/plaint_text_response.json @@ -0,0 +1 @@ +Plain text diff --git a/extra/modules/optable-targeting/src/test/resources/targeting_response.json b/extra/modules/optable-targeting/src/test/resources/targeting_response.json new file mode 100644 index 00000000000..a5959de45c9 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/resources/targeting_response.json @@ -0,0 +1,62 @@ +{ + "user": [ + + ], + "audience": [ + { + "provider": "optable.co", + "ids": [ + { + "id": "audience_id" + } + ], + "keyspace": "keyspace", + "rtb_segtax": 1 + } + ], + "ortb2": { + "user": { + "data": [ + { + "id": "data_id", + "segment": [ + { + "id": "segment_id" + } + ] + } + ], + "eids": [ + { + "source": "eid_source1", + "uids": [ + { + "id": "uid_id1", + "atype": 3, + "ext": { + "advertising_token": "advertising_token", + "refresh_token": "refresh_token", + "identity_expires": 1739281106209, + "refresh_from": 1739025506209, + "refresh_expires": 1741613906209, + "refresh_response_key": "refresh_response_key" + } + } + ] + }, + { + "source": "eid_source2", + "uids": [ + { + "id": "uid_id2", + "atype": 3, + "ext": { + "stype": "cto_bundle_hem_api" + } + } + ] + } + ] + } + } +} diff --git a/extra/modules/pom.xml b/extra/modules/pom.xml index 76485c54722..7e093b35658 100644 --- a/extra/modules/pom.xml +++ b/extra/modules/pom.xml @@ -24,6 +24,7 @@ pb-response-correction greenbids-real-time-data pb-request-correction + optable-targeting diff --git a/sample/configs/prebid-config-optable.yaml b/sample/configs/prebid-config-optable.yaml new file mode 100644 index 00000000000..df3852ace2c --- /dev/null +++ b/sample/configs/prebid-config-optable.yaml @@ -0,0 +1,91 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true + improvedigital: + enabled: true + colossus: + enabled: true + triplelift: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/configs/sample-app-settings-optable.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample/stored + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + optable-targeting: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 600, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] + }, + "auction-response": { + "groups": [ + { + "timeout": 10, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-auction-response-hook" + } + ] + } + ] + } + } + } + } + } + modules: + optable-targeting: + api-endpoint: endpoint + api-key: key + ppid-mapping: {"pubcid.org": "c"} + adserver-targeting: true diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml new file mode 100644 index 00000000000..2671a6a7642 --- /dev/null +++ b/sample/configs/sample-app-settings-optable.yaml @@ -0,0 +1,17 @@ +accounts: + - id: 1 + status: active + auction: + price-granularity: low + privacy: + ccpa: + enabled: true + gdpr: + enabled: true + cookie-sync: + default-limit: 8 + max-limit: 15 + coop-sync: + default: true + analytics: + allow-client-details: true diff --git a/sample/stored/optable-stored-response.json b/sample/stored/optable-stored-response.json new file mode 100644 index 00000000000..66c6a86b13b --- /dev/null +++ b/sample/stored/optable-stored-response.json @@ -0,0 +1,166 @@ +[ + { + "bid": + [ + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "12323-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.50", + "hb_size": "300x450" + }, + "type": "banner" + } + }, + "h": 450, + "id": "a961e06a-cee0-4160-9894-7a5ce7823c23", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.50, + "w": 300 + }, + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "50a4bd72-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.00", + "hb_size": "300x250" + }, + "type": "banner" + } + }, + "h": 250, + "id": "7fec12e0-e1d2-428f-b43c-0c2283843586", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.00, + "w": 300 + } + ], + "seat": "bidder1" + }, + { + "bid": + [ + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "12323-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.50", + "hb_size": "300x450" + }, + "type": "banner" + } + }, + "h": 450, + "id": "ba65ff9c-25f5-41ff-8c1d-21443479d7b9", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.50, + "w": 300 + }, + { + "adomain": + [ + "domain.com" + ], + "cid": "EFwGoMegjvRgamXpkklIsu", + "crid": "EIDzGDmsQQ64qWxicOan", + "exp": 900, + "ext": + { + "prebid": + { + "bidid": "f1b11176-a8de-4842-b16c-edef4ad4b53a", + "events": + {}, + "meta": + { + "adaptercode": "bidder" + }, + "targeting": + { + "hb_bidder": "bidder", + "hb_cache_host": "example.com", + "hb_cache_id": "50a4bd72-0dd8-443e-997b-a03440261a86", + "hb_cache_path": "", + "hb_pb": "1.00", + "hb_size": "300x250" + }, + "type": "banner" + } + }, + "h": 250, + "id": "374b5f85-e0b9-4f25-98ca-863c0698005a", + "impid": "035917ec-b770-4ecf-b762-0b12c5443b68", + "iurl": "https://example.com/i/3dc5016d-c23a-4740-9f6a-4b00c87f47ac/RPjQMBzajgbIvlj.jpeg", + "price": 1.00, + "w": 300 + } + ], + "seat": "bidder2" + } +] From db44556dd8ba3dc0049a63e65dc81941caba3a40 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Fri, 7 Mar 2025 09:07:49 +0100 Subject: [PATCH 02/41] optable-targeting: remove reg attribute from targeting query --- .../targeting/model/OptableAttributes.java | 10 +- ...eTargetingProcessedAuctionRequestHook.java | 2 +- .../v1/core/OptableAttributesResolver.java | 73 ++--------- .../targeting/v1/core/QueryBuilder.java | 14 +- .../core/OptableAttributesResolverTest.java | 121 ++---------------- .../v1/core/OptableTargetingTest.java | 2 +- .../targeting/v1/core/QueryBuilderTest.java | 7 +- 7 files changed, 38 insertions(+), 191 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index 967e53dd544..2ac368f3a6e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -10,19 +10,19 @@ @Builder(toBuilder = true) public class OptableAttributes { - String reg; - String gpp; Set gppSid; - String tcf; + String gdprConsent; + + boolean gdprApplies; List ips; Long timeout; - public static OptableAttributes of(String reg) { - return OptableAttributes.builder().reg(reg).build(); + public static OptableAttributes of() { + return OptableAttributes.builder().build(); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 9d6279073ca..ae0705006e3 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -57,7 +57,7 @@ public Future> call(AuctionRequestPayloa } final long timeout = getHookRemainTime(invocationContext); - final OptableAttributes attributes = optableAttributesResolver.reloveAttributes( + final OptableAttributes attributes = optableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), properties.getTimeout()); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 2989943aa96..243ed1c5303 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -2,10 +2,8 @@ import com.iab.gpp.encoder.GppModel; import lombok.AllArgsConstructor; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.privacy.model.PrivacyContext; @@ -18,60 +16,19 @@ public class OptableAttributesResolver { private final IpResolver ipResolver; - private static final int SECTION_ID_CANADA = 5; - - private static final Set SECTION_ID_EUROPE = Set.of(1, 2); - - private static final Set SECTION_ID_US = - Set.of(6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22); - - private static final Set GDPR_COUNTRIES = Set.of("Belgium", "Bulgaria", "Cyprus", "Denmark", - "Germany", "Estonia", "Finland", "France", "Greece", "Hungary", "Ireland", "Italy", "Croatia", "Latvia", - "Liechtenstein", "Lithuania", "Luxembourg", "Malta", "The Netherlands", "Norway", - "Austria", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Switzerland", "UK"); - - private static final Set GDPR_REGIONS = Set.of("Azores", "Canary", "Islands", "Guadeloupe", "French Guiana", - "Madeira", "Martinique", "Mayotte", "Reunion", "Saint Martin"); - - public OptableAttributes reloveAttributes(AuctionContext auctionContext, Long timeout) { + public OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { final List ips = ipResolver.resolveIp(auctionContext); - OptableAttributes optableAttributes = getTcfPrivacyAttributes(auctionContext); + OptableAttributes optableAttributes = getGdprPrivacyAttributes(auctionContext); if (optableAttributes == null) { optableAttributes = getGppPrivacyAttributes(auctionContext); } - if (optableAttributes == null) { - optableAttributes = getGeoIpPrivacyAttributes(auctionContext); - } return optableAttributes != null ? optableAttributes.toBuilder().ips(ips).timeout(timeout).build() : OptableAttributes.builder().ips(ips).timeout(timeout).build(); } - private OptableAttributes getGeoIpPrivacyAttributes(AuctionContext auctionContext) { - final Optional geoInfoOpt = Optional.ofNullable(auctionContext).map(AuctionContext::getGeoInfo); - - final String country = geoInfoOpt.map(GeoInfo::getCountry).orElse(null); - final String region = geoInfoOpt.map(GeoInfo::getRegion).orElse(null); - - if (StringUtils.isNotEmpty(country)) { - if (country.equalsIgnoreCase("US") || country.equalsIgnoreCase("United States")) { - return OptableAttributes.of("us"); - } else if (country.equalsIgnoreCase("Quebec") || country.equalsIgnoreCase("Canada")) { - return OptableAttributes.of("can"); - } else if (GDPR_COUNTRIES.contains(country)) { - return OptableAttributes.of("gdpr"); - } - } - - if (StringUtils.isNotEmpty(region) && GDPR_REGIONS.contains(region)) { - return OptableAttributes.of("gdpr"); - } - - return null; - } - private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) { final Optional gppContextOpt = Optional.ofNullable(auctionContext) .map(AuctionContext::getGppContext); @@ -89,38 +46,26 @@ private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) .map(GppContext.Scope::getSectionsIds) .orElse(Set.of()); - return OptableAttributes.of(sidsToReg(sids)).toBuilder().gpp(gppConsent).gppSid(sids).build(); + return OptableAttributes.builder().gpp(gppConsent).gppSid(sids).build(); } return null; } - private OptableAttributes getTcfPrivacyAttributes(AuctionContext auctionContext) { + private OptableAttributes getGdprPrivacyAttributes(AuctionContext auctionContext) { return Optional.ofNullable(auctionContext) .map(AuctionContext::getPrivacyContext) .map(PrivacyContext::getTcfContext) .map(ctx -> { + if (ctx.isConsentValid()) { - return OptableAttributes.of("gdpr").toBuilder().tcf(ctx.getConsentString()).build(); + return OptableAttributes.builder() + .gdprConsent(ctx.getConsentString()) + .gdprApplies(ctx.isInGdprScope()) + .build(); } return null; }).orElse(null); } - - private String sidsToReg(Set sids) { - if (sids == null) { - return null; - } - - if (sids.contains(SECTION_ID_CANADA)) { - return "can"; - } else if (sids.stream().anyMatch(SECTION_ID_EUROPE::contains)) { - return "gdpr"; - } else if (sids.stream().anyMatch(SECTION_ID_US::contains)) { - return "us"; - } - - return null; - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index ab71876f640..a3355914d8b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -54,17 +54,19 @@ private List reorderIds(List ids) { } private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes) { - Optional.ofNullable(optableAttributes.getReg()).ifPresent(reg -> sb.append("®=").append(reg)); - Optional.ofNullable(optableAttributes.getTcf()).ifPresent(tcf -> sb.append("&tcf=").append(tcf)); - Optional.ofNullable(optableAttributes.getGpp()).ifPresent(tcf -> sb.append("&gpp=").append(tcf)); + Optional.ofNullable(optableAttributes.getGdprConsent()).ifPresent(consent -> + sb.append("&gdpr_consent=").append(consent)); + Optional.of(optableAttributes.isGdprApplies()).ifPresent(applies -> + sb.append("&gdprApplies=").append(applies ? 1 : 0)); + Optional.ofNullable(optableAttributes.getGpp()).ifPresent(tcf -> + sb.append("&gpp=").append(tcf)); Optional.ofNullable(optableAttributes.getGppSid()).ifPresent(gppSids -> { if (CollectionUtils.isNotEmpty(gppSids)) { sb.append("&gpp_sid=").append(gppSids.stream().findFirst()); } }); - Optional.ofNullable(optableAttributes.getTimeout()).ifPresent(timeout -> { - sb.append("&timeout=").append(timeout).append("ms"); - }); + Optional.ofNullable(optableAttributes.getTimeout()).ifPresent(timeout -> + sb.append("&timeout=").append(timeout).append("ms")); } private void buildQueryString(StringBuilder sb, List ids) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 474596b2dd7..ed51ed970d7 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -51,16 +51,17 @@ public void setUp() { public void shouldResolveTcfAttributesWhenConsentIsValid() { // given when(tcfContext.isConsentValid()).thenReturn(true); + when(tcfContext.isInGdprScope()).thenReturn(true); when(tcfContext.getConsentString()).thenReturn("consent"); final AuctionContext auctionContext = givenAuctionContext(tcfContext); // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = target.resolveAttributes(auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() - .returns("gdpr", OptableAttributes::getReg) - .returns("consent", OptableAttributes::getTcf); + .returns(true, OptableAttributes::isGdprApplies) + .returns("consent", OptableAttributes::getGdprConsent); } @Test @@ -71,17 +72,17 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { final AuctionContext auctionContext = givenAuctionContext(tcfContext); // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = target.resolveAttributes(auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() - .returns(null, OptableAttributes::getReg) - .returns(null, OptableAttributes::getTcf) + .returns(false, OptableAttributes::isGdprApplies) + .returns(null, OptableAttributes::getGdprConsent) .returns(List.of("8.8.8.8"), OptableAttributes::getIps); } @Test - public void shouldResolveGppGdprAttributes() { + public void shouldResolveGppAttributes() { // given final GppModel gppModel = mock(); when(gppModel.encode()).thenReturn("consent"); @@ -90,117 +91,15 @@ public void shouldResolveGppGdprAttributes() { // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = target.resolveAttributes(auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() - .returns("gdpr", OptableAttributes::getReg) + .returns(false, OptableAttributes::isGdprApplies) .returns("consent", OptableAttributes::getGpp) .returns(Set.of(1), OptableAttributes::getGppSid); } - @Test - public void shouldResolveGppCanadaAttributes() { - // given - final GppModel gppModel = mock(); - when(gppModel.encode()).thenReturn("consent"); - when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(5))); - final AuctionContext auctionContext = givenAuctionContext(gppContext); - - // when - - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); - - // then - assertThat(result).isNotNull() - .returns("can", OptableAttributes::getReg) - .returns("consent", OptableAttributes::getGpp) - .returns(Set.of(5), OptableAttributes::getGppSid); - } - - @Test - public void shouldResolveGppUSAttributes() { - // given - final GppModel gppModel = mock(); - when(gppModel.encode()).thenReturn("consent"); - when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(8))); - final AuctionContext auctionContext = givenAuctionContext(gppContext); - - // when - - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); - - // then - assertThat(result).isNotNull() - .returns("us", OptableAttributes::getReg) - .returns("consent", OptableAttributes::getGpp) - .returns(Set.of(8), OptableAttributes::getGppSid); - } - - @Test - public void shouldResolveGeoInfoUSAttributes() { - // given - when(geoInfo.getCountry()).thenReturn("United States"); - final AuctionContext auctionContext = givenAuctionContext(geoInfo); - - // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); - - // then - assertThat(result).isNotNull() - .returns("us", OptableAttributes::getReg) - .returns(null, OptableAttributes::getGpp) - .returns(null, OptableAttributes::getTcf); - } - - @Test - public void shouldResolveGeoInfoGDPRAttributes() { - // given - when(geoInfo.getCountry()).thenReturn("Malta"); - final AuctionContext auctionContext = givenAuctionContext(geoInfo); - - // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); - - // then - assertThat(result).isNotNull() - .returns("gdpr", OptableAttributes::getReg) - .returns(null, OptableAttributes::getGpp) - .returns(null, OptableAttributes::getTcf); - } - - @Test - public void shouldResolveGeoInfoCanadaAttributes() { - // given - when(geoInfo.getCountry()).thenReturn("Quebec"); - final AuctionContext auctionContext = givenAuctionContext(geoInfo); - - // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); - - // then - assertThat(result).isNotNull() - .returns("can", OptableAttributes::getReg) - .returns(null, OptableAttributes::getGpp) - .returns(null, OptableAttributes::getTcf); - } - - @Test - public void shouldResolveGeoInfoGDPRForRegionAttributes() { - // given - when(geoInfo.getRegion()).thenReturn("Mayotte"); - final AuctionContext auctionContext = givenAuctionContext(geoInfo); - - // when - final OptableAttributes result = target.reloveAttributes(auctionContext, properties.getTimeout()); - - // then - assertThat(result).isNotNull() - .returns("gdpr", OptableAttributes::getReg) - .returns(null, OptableAttributes::getGpp) - .returns(null, OptableAttributes::getTcf); - } - public AuctionContext givenAuctionContext(TcfContext tcfContext) { return AuctionContext.builder() .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 7a2cfd08921..a176683bef3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -107,7 +107,7 @@ public void shouldNotFailWhenApiClientReturnsFailFuture() { } private OptableAttributes givenOptableAttributes() { - return OptableAttributes.of("gdpr").toBuilder() + return OptableAttributes.builder() .gpp("gpp") .gppSid(Set.of(2)) .build(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index d7568d67307..d357b8e802b 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -42,7 +42,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final String query = target.build(ids, optableAttributes); // then - assertThat(query).contains("®=gdpr", "&tcf=tcf", "&timeout=100ms"); + assertThat(query).contains("&gdprApplies=1", "&gdpr_consent=tcf", "&timeout=100ms"); } @Test @@ -72,9 +72,10 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmpty() { } private OptableAttributes givenOptableAttributes() { - return OptableAttributes.of("gdpr").toBuilder() + return OptableAttributes.builder() .timeout(100L) - .tcf("tcf") + .gdprApplies(true) + .gdprConsent("tcf") .build(); } } From ca8e2277b8dfb2641fe3f84f21f22ff8b968350b Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Fri, 7 Mar 2025 15:47:28 +0100 Subject: [PATCH 03/41] optable-targeting: rename gdpr query attribute --- .../hooks/modules/optable/targeting/v1/core/QueryBuilder.java | 2 +- .../modules/optable/targeting/v1/core/QueryBuilderTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index a3355914d8b..5ea1ad75027 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -57,7 +57,7 @@ private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes Optional.ofNullable(optableAttributes.getGdprConsent()).ifPresent(consent -> sb.append("&gdpr_consent=").append(consent)); Optional.of(optableAttributes.isGdprApplies()).ifPresent(applies -> - sb.append("&gdprApplies=").append(applies ? 1 : 0)); + sb.append("&gdpr=").append(applies ? 1 : 0)); Optional.ofNullable(optableAttributes.getGpp()).ifPresent(tcf -> sb.append("&gpp=").append(tcf)); Optional.ofNullable(optableAttributes.getGppSid()).ifPresent(gppSids -> { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index d357b8e802b..6673883c95b 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -42,7 +42,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final String query = target.build(ids, optableAttributes); // then - assertThat(query).contains("&gdprApplies=1", "&gdpr_consent=tcf", "&timeout=100ms"); + assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); } @Test From b3776e628813d2af893f586e5d36ecac9262d45c Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 10 Mar 2025 22:01:02 +0100 Subject: [PATCH 04/41] optable-targeting: Increased modules version --- extra/modules/optable-targeting/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index 010eb5e8383..a394616c5f3 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.22.0-SNAPSHOT + 3.23.0-SNAPSHOT optable-targeting From e2b5adc4e82563934383617b43b629d12f5c9c43 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 27 Mar 2025 10:32:11 +0100 Subject: [PATCH 05/41] optable-targeting: update README.MD --- extra/modules/optable-targeting/README.md | 284 ++++++++++++++++++++++ 1 file changed, 284 insertions(+) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index e69de29bb2d..3ae7bd5f659 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -0,0 +1,284 @@ +## Overview +Optable module operates using a DCN backend API. Please contact your account manager to get started. + +The optable-targeting module enriches an incoming OpenRTB request by adding to the `user.eids` and `user.data` +objects. Under the hood the module extracts PPIDs (publisher provided IDs) from the incoming request's `user.ext.eids`, +and also if present sha256-hashed email, sha256-hashed phone, zip or Optable Visitor ID provided correspondingly in the +`user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields (a full list of IDs is given in a table below). These IDs are +sent as input to the Targeting API. The received response data is used to enrich the OpenRTB request and response. +Targeting API endpoint is configurable per publisher. + +## Setup + +### Execution Plan + +This module runs at two stages: + +* Processed Auction Request: to enrich `user.eids` and `user.data`. +* Auction Response: to inject ad server targeting. + +We recommend defining the execution plan in the account config so the module is only invoked for specific accounts. See +below for an example. + +### Global Config + +There is no host-company level config for this module. + +### Account-Level Config + +To start using current module in PBS-Java you have to enable module and add +`optable-targeting-processed-auction-request-hook` and `optable-targeting-auction-response-hook` into hooks execution +plan inside your config file: +Here's a general template for the account config used in PBS-Java: + +```yaml +hooks: + optable-targeting: + enabled: true + host-execution-plan: > + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 100, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] + }, + "auction-response": { + "groups": [ + { + "timeout": 10, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-auction-response-hook" + } + ] + } + ] + } + } + } + } + } +``` + +Sample module enablement configuration in JSON and YAML formats: + +```json +{ + "modules": + { + "optable-targeting": + { + "api-endpoint": "endpoint", + "api-key": "key", + "timeout": 50, + "ppid-mapping": { + "pubcid.org": "c" + }, + "adserver-targeting": false + } + } +} +``` + +```yaml + modules: + optable-targeting: + api-endpoint: endpoint + api-key: key + timeout: 50 + ppid-mapping: { + "pubcid.org": "c" + } + adserver-targeting: false +``` + +### Timeout considerations + +The timeout value specified in the execution plan for the `processed-auction-request` hook is very important to be +picked such that the hook has enough time to make a roundtrip to Optable Targeting Edge API over HTTP. + +**Note:** Do not confuse hook timeout value with the module timeout parameter which is optional. The hook timeout value +would depend on the cloud/region where the PBS instance is hosted and the latency to reach the Optable's servers. This +will need to be verified experimentally upon deployment. + +The timeout value for the `auction-response` can be set to 10 ms - usually it will be sub-millisecond time as there are +no HTTP calls made in this hook - Optable-specific keywords are cached on the `processed-auction-request` stage and +retrieved from the module invocation context later. + +## Module Configuration Parameters for PBS-Java + +The parameter names are specified with full path using dot-notation. F.e. `section-name` .`sub-section` .`param-name` +would result in this nesting in the JSON configuration: + +```json +{ + "section-name": { + "sub-section": { + "param-name": "param-value" + } + } +} +``` + + +| Param Name | Required | Type | Default value | Description | +|:-------------------|:---------|:--------|:---------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| api-endpoint | yes | string | none | Optable Targeting Edge API endpoint URL, required | +| api-key | no | string | none | If the API is protected with a key - this param needs to be specified to be sent in the auth header | +| ppid-mapping | no | map | none | This specifies PPID source (`user.ext.eids[].source`) to a custom identifier prefix mapping, f.e. `{"example.com" : "c"}`. See the section on ID Mapping below for more detail. | +| adserver-targeting | no | boolean | false | If set to true - will add the Optable-specific adserver targeting keywords into the PBS response for every `seatbid[].bid[].ext.prebid.targeting` | +| timeout | no | integer | false | A soft timeout (in ms) sent as a hint to the Targeting API endpoint to limit the request times to Optable's external tokenizer services | +| id-prefix-order | no | string | none | An optional string of comma separated id prefixes that prioritizes and specifies the order in which ids are provided to Targeting API in a query string. F.e. "c,c1,id5" will guarantee that Targeting API will see id=c:...,c1:...,id5:... if these ids are provided. id-prefixes not mentioned in this list will be added in arbitrary order after the priority prefix ids. This affects Targeting API processing logic | + +## ID Mapping + +Internally the module sends requests to Optable Targeting API. The output of Targeting API is used to enrich the request +and response. The below table describes the parameters that the module automatically fetches from OpenRTB request and +then sends to the Targeting API. The module will use a prefix as specified in the table to prepend the corresponding ID +value when sending it to the Targeting API in the form `id=prefix:value`. + +See [Optable documentation](https://docs.optable.co/optable-documentation/dmp/reference/identifier-types#type-prefixes) +on identifier types. Targeting API accepts multiple id parameters - and their order may affect the results, thus +`id-prefix-order` specifies the order of the ids. + + +| Identifier Type | OpenRTB field | ID Type Prefix | +|--------------------------------------------------------------------------------|-----------------------------------------------------------------------|------------------------------------------| +| Email Address | `user.ext.optable.email` | `e:` | +| Phone Number | `user.ext.optable.phone` | `p:` | +| Postal Code | `user.ext.optable.zip` | `z:` | +| IPv4 Address | `device.ip` | ~~i4:~~ Sent as `X-Forwarded-For` header | +| IPv6 Address | `device.ipv6` | ~~i6:~~ Sent as `X-Forwarded-For` header | +| Apple IDFA | `device.ifa if lcase(device.os) contains 'ios' and device.lmt!=1` | `a:` | +| Google GAID | `device.ifa if lcase(device.os) contains 'android' and device.lmt!=1` | `g:` | +| Roku RIDA | `device.ifa if lcase(device.os) contains 'roku' and device.lmt!=1` | `r:` | +| Samsung TV TIFA | `device.ifa if lcase(device.os) contains 'tizen' and device.lmt!=1` | `s:` | +| Amazon Fire AFAI | `device.ifa if lcase(device.os) contains 'fire' and device.lmt!=1` | `f:` | +| [NetID](https://docs.prebid.org/dev-docs/modules/userid-submodules/netid.html) | `user.ext.eids[].uids[0] when user.ext.eids[].source="netid.de"` | `n:` | +| [ID5](https://docs.prebid.org/dev-docs/modules/userid-submodules/id5.html) | `user.ext.eids[].uids[0] when user.ext.eids[].source="id5-sync.com"` | `id5:` | +| [Utiq](https://docs.prebid.org/dev-docs/modules/userid-submodules/utiq.html) | `user.ext.eids[].uids[0] when user.ext.eids[].source="utiq.com"` | `utiq:` | +| Optable VID | `user.ext.optable.vid` | `v:` | + +### Optable input erasure + +**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields will be removed by the module from the original +OpenRTB request before being sent to bidders. + +### Publisher Provided IDs (PPID) Mapping + +Custom user IDs are sent in the OpenRTB request in the +[`user.ext.eids[]`](https://github.com/InteractiveAdvertisingBureau/openrtb2.x/blob/main/2.6.md#3227---object-eid-). +The `ppid-mapping` allows to specify the mapping of a source to one of the custom identifier type prefixes `c`-`c19` - +see [documentation](https://docs.optable.co/optable-documentation/dmp/reference/identifier-types#type-prefixes), f.e.: + +```yaml +ppid-mapping: {"example.com": "c2", "test.com": "c3"} +``` + +It is also possible to override any of the automatically retrieved `user.ext.eids[]` mentioned in the table above (s.a. +id5, utiq) so they are mapped to a different prefix. f.e. `id5-sync.com` can be mapped to a prefix other than `id5:`, +like: + +```yaml +ppid-mapping: {"id5-sync.com": "c1"} +``` + +This will lead to id5 ID supplied as `id=c1:...` to the Targeting API. + +## Analytics Tags + +The following 2 analytics tags are written by the module: + +* `optable-enrich-request` +* `optable-enrich-response` + +The `status` is either `success` or `failure`. Where it is `failure` a `results[0].value.reason` is provided. +For the `optable-enrich-request` activity the `execution-time` value is logged. +Example: + +```json +{ + "analytics": { + "tags": [ + { + "stage": "auction-response", + "module": "optable-targeting", + "analyticstags": { + "activities": [ + { + "name": "optable-enrich-request", + "status": "success", + "results": [ + { + "values": { + "execution-time": 33 + } + } + ] + }, + { + "name": "optable-enrich-response", + "status": "success", + "results": [ + { + "values": { + "reason": "none" + } + } + ] + } + ] + } + } + ] + } +} +``` + +If `adserver-targeting` was set to `false` in the config `optable-enrich-response` analytics tag is not written. + +## Running the demo (PBS-Java) + +1. Build the server bundle JAR as described in [Build Project](https://github.com/prebid/prebid-server-java/blob/master/docs/build.md#build-project), e.g. + +```bash +mvn clean package --file extra/pom.xml +``` + +2. In the `sample/configs/prebid-config-optable.yaml` file specify the `api-endpoint` URL of your DCN, f.e.: + +```yaml +api-endpoint: https://example.com/v2/targeting +``` + +3. Start server bundle JAR as described in [Running project](https://github.com/prebid/prebid-server-java/blob/master/docs/run.md#running-project), e.g. + +```bash +java -jar target/prebid-server-bundle.jar --spring.config.additional-location=sample/configs/prebid-config-with-optable.yaml +``` + +4. Run sample request against the server as described in [the sample directory](https://github.com/prebid/prebid-server-java/tree/master/sample), e.g. + +```bash +curl http://localhost:8080/openrtb2/auction --data @extra/modules/optable-targeting/sample-requests/data.json +``` + +5. Observe the `user.eids` and `user.data` objects enriched. + +## Maintainer contacts + +Any suggestions or questions can be directed to [prebid@optable.co](mailto:prebid@optable.co). + +Alternatively please open a new [issue](https://github.com/prebid/prebid-server-java/issues/new) or [pull request](https://github.com/prebid/prebid-server-java/pulls) in this repository. From 9cc83df55bb2fadc99c042c37716e270f2c92473 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 27 Mar 2025 10:47:36 +0100 Subject: [PATCH 06/41] optable-targeting: Code cleanup --- extra/modules/optable-targeting/pom.xml | 62 ------------- .../config/OptableTargetingConfig.java | 30 ++----- .../targeting/model/EnrichmentStatus.java | 2 +- .../modules/optable/targeting/model/Id.java | 8 +- .../targeting/model/ModuleContext.java | 35 +++++++- .../targeting/model/OptableAttributes.java | 2 +- .../targeting/model/net/HttpResponse.java | 4 +- .../targeting/model/net/OptableCall.java | 5 +- .../model/openrtb/ExtUserOptable.java | 4 - .../model/openrtb/TargetingResult.java | 2 - .../OptableTargetingAuctionResponseHook.java | 42 +++++---- .../targeting/v1/OptableTargetingModule.java | 14 ++- ...eTargetingProcessedAuctionRequestHook.java | 89 ++++++++++--------- .../v1/analytics/AnalyticTagsResolver.java | 8 +- .../v1/analytics/AnalyticsTagsBuilder.java | 47 +++++++--- .../v1/core/AuctionResponseValidator.java | 9 +- .../optable/targeting/v1/core/IdsMapper.java | 26 ++++-- .../targeting/v1/core/IdsResolver.java | 2 +- .../optable/targeting/v1/core/IpResolver.java | 5 +- .../v1/core/OptableAttributesResolver.java | 60 ++++++------- .../targeting/v1/core/OptableTargeting.java | 9 +- .../targeting/v1/core/PayloadResolver.java | 37 +++----- .../targeting/v1/core/QueryBuilder.java | 45 +++++----- .../v1/core/merger/BidRequestBuilder.java | 17 ++-- ...eBuilder.java => BidResponseResolver.java} | 62 +++++++------ .../targeting/v1/core/merger/DataMerger.java | 25 +++--- .../targeting/v1/core/merger/EidsMerger.java | 17 ++-- .../{BaseMerger.java => ExtMerger.java} | 7 +- .../optable/targeting/v1/net/APIClient.java | 16 +++- ...tableTargetingAuctionResponseHookTest.java | 13 +-- .../v1/OptableTargetingModuleTest.java | 35 +++++++- ...getingProcessedAuctionRequestHookTest.java | 10 +-- .../v1/core/AuctionResponseValidatorTest.java | 20 ++--- .../core/OptableAttributesResolverTest.java | 2 +- .../v1/core/merger/DataMergerTest.java | 20 ++--- .../v1/core/merger/EidsMergerTest.java | 24 ++--- .../v1/core/merger/ExtMergerTest.java | 16 +--- .../targeting/v1/net/APIClientTest.java | 5 +- .../v1/net/OptableResponseParserTest.java | 4 +- ...e.yaml => prebid-config-with-optable.yaml} | 0 40 files changed, 427 insertions(+), 413 deletions(-) rename extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/{BidResponseBuilder.java => BidResponseResolver.java} (51%) rename extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/{BaseMerger.java => ExtMerger.java} (77%) rename sample/configs/{prebid-config-optable.yaml => prebid-config-with-optable.yaml} (100%) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index a394616c5f3..fa7d5cf73ac 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -12,66 +12,4 @@ optable-targeting Optable targeting module - - - - - 3.4.0 - 10.17.0 - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - ${checkstyle-plugin.version} - - - ../checkstyle.xml - UTF-8 - true - - true - false - true - - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - - - - - - - org.apache.maven.plugins - maven-checkstyle-plugin - - - validate - - checkstyle - - - - - - - - - - com.fasterxml.jackson.dataformat - jackson-dataformat-xml - - - com.puppycrawl.tools - checkstyle - ${checkstyle.version} - - diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index b853f0d36fb..2d6f55034e8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -3,21 +3,20 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.IpResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseParser; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.spring.config.VertxContextScope; import org.prebid.server.spring.config.model.HttpClientProperties; import org.prebid.server.util.HttpUtil; @@ -39,7 +38,7 @@ public class OptableTargetingConfig { @Bean ObjectMapper objectMapper() { - return new ObjectMapper(); + return ObjectMapperProvider.mapper(); } @Bean @@ -89,39 +88,28 @@ APIClient apiClient(OptableHttpClientWrapper httpClientWrapper, } @Bean - OptableAttributesResolver optableAttributesResolver(IpResolver ipResolver) { - return new OptableAttributesResolver(ipResolver); + OptableAttributesResolver optableAttributesResolver() { + return new OptableAttributesResolver(); } - @Bean() + @Bean OptableTargeting optableTargeting(IdsMapper parametersExtractor, QueryBuilder queryBuilder, APIClient apiClient) { return new OptableTargeting(parametersExtractor, queryBuilder, apiClient); } - @Bean - IpResolver ipResolver() { - return new IpResolver(); - } - - @Bean - AuctionResponseValidator auctionResponseValidator() { - return new AuctionResponseValidator(); - } - @Bean OptableTargetingModule optableTargetingModule(OptableTargetingProperties properties, AnalyticTagsResolver analyticTagsResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, - OptableAttributesResolver optableAttributesResolver, - AuctionResponseValidator auctionResponseValidator) { + OptableAttributesResolver optableAttributesResolver) { return new OptableTargetingModule(List.of( new OptableTargetingProcessedAuctionRequestHook(properties, optableTargeting, payloadResolver, optableAttributesResolver), new OptableTargetingAuctionResponseHook(analyticTagsResolver, payloadResolver, - properties.getAdserverTargeting(), auctionResponseValidator))); + properties.getAdserverTargeting()))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java index 08affde67f1..97b81900558 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java @@ -5,7 +5,7 @@ @Builder(toBuilder = true) public record EnrichmentStatus(Status status, Reason reason) { - public static EnrichmentStatus fail() { + public static EnrichmentStatus failure() { return EnrichmentStatus.builder().status(Status.FAIL).build(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java index ebd0d53ae05..d80057fddde 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Id.java @@ -1,14 +1,10 @@ package org.prebid.server.hooks.modules.optable.targeting.model; -import lombok.AllArgsConstructor; -import lombok.Builder; import lombok.Value; import javax.validation.constraints.NotNull; -@AllArgsConstructor(staticName = "of") -@Builder(toBuilder = true) -@Value +@Value(staticConstructor = "of") public class Id { public static final String EMAIL = "e"; @@ -39,10 +35,8 @@ public class Id { public static final String OPTABLE_VID = "v"; - /** Name of Identifier */ @NotNull String name; - /** Identifier's value */ String value; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index f58b0d6a4ee..1f5eb064b73 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -1,14 +1,12 @@ package org.prebid.server.hooks.modules.optable.targeting.model; import lombok.Getter; -import lombok.Setter; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import java.util.List; import java.util.Objects; -@Setter @Getter public class ModuleContext { @@ -25,4 +23,37 @@ public class ModuleContext { public static ModuleContext of(AuctionInvocationContext invocationContext) { return (ModuleContext) Objects.requireNonNull(invocationContext.moduleContext()); } + + public ModuleContext setMetrics(Metrics metrics) { + this.metrics = metrics; + return this; + } + + public ModuleContext setTargeting(List targeting) { + this.targeting = targeting; + return this; + } + + public ModuleContext setEnrichRequestStatus(EnrichmentStatus enrichRequestStatus) { + this.enrichRequestStatus = enrichRequestStatus; + return this; + } + + public ModuleContext setEnrichResponseStatus(EnrichmentStatus enrichResponseStatus) { + this.enrichResponseStatus = enrichResponseStatus; + return this; + } + + public ModuleContext setAdserverTargetingEnabled(boolean adserverTargetingEnabled) { + this.adserverTargetingEnabled = adserverTargetingEnabled; + return this; + } + + public ModuleContext setFinishTime(long timestamp) { + setMetrics(getMetrics().toBuilder() + .moduleFinishTime(timestamp) + .build()); + + return this; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index 2ac368f3a6e..2ef95eb429d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -22,7 +22,7 @@ public class OptableAttributes { Long timeout; - public static OptableAttributes of() { + public static OptableAttributes empty() { return OptableAttributes.builder().build(); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java index 35390f48e3f..172b09265ec 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java @@ -1,11 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.model.net; import io.vertx.core.MultiMap; -import lombok.AllArgsConstructor; import lombok.Value; -@AllArgsConstructor(staticName = "of") -@Value +@Value(staticConstructor = "of") public class HttpResponse { int statusCode; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java index d67e5e22818..37cca225dca 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java @@ -14,10 +14,7 @@ public class OptableCall { OptableError error; - public static OptableCall succeededHttp(HttpRequest request, - HttpResponse response, - OptableError error) { - + public static OptableCall succeededHttp(HttpRequest request, HttpResponse response, OptableError error) { return new OptableCall(request, response, error); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java index 00281604b94..787b3adbf1a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java @@ -5,10 +5,6 @@ import lombok.Value; import org.prebid.server.proto.openrtb.ext.FlexibleExtension; -/** - * An object containing identifiers - * which are provided by Optable - */ @EqualsAndHashCode(callSuper = true) @Builder(toBuilder = true) @Value diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java index c5ab2959117..826ce2698ed 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java @@ -1,12 +1,10 @@ package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; -import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.Value; import java.util.List; @Value -@JsonIgnoreProperties(ignoreUnknown = true) public class TargetingResult { List audience; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index d4addf7d4c6..eb57fee4970 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -1,7 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import io.vertx.core.Future; -import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; @@ -20,24 +19,27 @@ import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; import java.util.List; +import java.util.Objects; import java.util.function.Function; -@AllArgsConstructor public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { private static final String CODE = "optable-targeting-auction-response-hook"; - private AnalyticTagsResolver analyticTagsResolver; + private final AnalyticTagsResolver analyticTagsResolver; - private PayloadResolver payloadResolver; + private final PayloadResolver payloadResolver; - boolean adserverTargeting; + private final boolean adserverTargeting; - private AuctionResponseValidator auctionResponseValidator; + public OptableTargetingAuctionResponseHook( + AnalyticTagsResolver analyticTagsResolver, + PayloadResolver payloadResolver, + boolean adserverTargeting) { - @Override - public String code() { - return CODE; + this.analyticTagsResolver = Objects.requireNonNull(analyticTagsResolver); + this.payloadResolver = Objects.requireNonNull(payloadResolver); + this.adserverTargeting = adserverTargeting; } @Override @@ -48,31 +50,31 @@ public Future> call(AuctionResponsePayl moduleContext.setAdserverTargetingEnabled(adserverTargeting); if (adserverTargeting) { - final EnrichmentStatus validationStatus = auctionResponseValidator.checkEnrichmentPossibility( + final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); moduleContext.setEnrichResponseStatus(validationStatus); if (validationStatus.status() == Status.SUCCESS) { - return enrichedPayloadFuture(moduleContext); + return enrichedPayload(moduleContext); } } - return successFeature(moduleContext); + return success(moduleContext); } - private Future> enrichedPayloadFuture(ModuleContext moduleContext) { + private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); return CollectionUtils.isNotEmpty(targeting) - ? updateFeature(payload -> enrichPayload(payload, targeting), moduleContext) - : successFeature(moduleContext); + ? update(payload -> enrichPayload(payload, targeting), moduleContext) + : success(moduleContext); } private AuctionResponsePayload enrichPayload(AuctionResponsePayload payload, List targeting) { return AuctionResponsePayloadImpl.of(payloadResolver.enrichBidResponse(payload.bidResponse(), targeting)); } - private Future> updateFeature( + private Future> update( Function func, ModuleContext moduleContext) { return Future.succeededFuture( @@ -85,8 +87,7 @@ private Future> updateFeature( .build()); } - private Future> successFeature(ModuleContext moduleContext) { - + private Future> success(ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) @@ -95,4 +96,9 @@ private Future> successFeature(ModuleCo .analyticsTags(analyticTagsResolver.resolve(moduleContext)) .build()); } + + @Override + public String code() { + return CODE; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java index 32fccda8375..0b36b276b57 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModule.java @@ -6,11 +6,21 @@ import java.util.Collection; -public record OptableTargetingModule( - Collection> hooks) implements Module { +public class OptableTargetingModule implements Module { public static final String CODE = "optable-targeting"; + private final Collection> hooks; + + public OptableTargetingModule(Collection> hooks) { + this.hooks = hooks; + } + + @Override + public Collection> hooks() { + return hooks; + } + @Override public String code() { return CODE; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index ae0705006e3..1ba41286154 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -2,7 +2,6 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; -import lombok.AllArgsConstructor; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -22,10 +21,10 @@ import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; -@AllArgsConstructor public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { private static final String CODE = "optable-targeting-processed-auction-request-hook"; @@ -34,29 +33,38 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final OptableTargetingProperties properties; - private OptableTargeting optableTargeting; + private final OptableTargeting optableTargeting; - private PayloadResolver payloadResolver; + private final PayloadResolver payloadResolver; private final OptableAttributesResolver optableAttributesResolver; - @Override - public String code() { - return CODE; + public OptableTargetingProcessedAuctionRequestHook(OptableTargetingProperties properties, + OptableTargeting optableTargeting, + PayloadResolver payloadResolver, + OptableAttributesResolver optableAttributesResolver) { + + this.properties = Objects.requireNonNull(properties); + this.optableTargeting = Objects.requireNonNull(optableTargeting); + this.payloadResolver = Objects.requireNonNull(payloadResolver); + this.optableAttributesResolver = Objects.requireNonNull(optableAttributesResolver); } @Override public Future> call(AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - final ModuleContext moduleContext = createModuleContext(); + final ModuleContext moduleContext = new ModuleContext() + .setMetrics(Metrics.builder() + .moduleStartTime(System.currentTimeMillis()) + .build()); final BidRequest bidRequest = getBidRequest(auctionRequestPayload); if (bidRequest == null) { - return failedFeature(moduleContext); + return failure(moduleContext); } - final long timeout = getHookRemainTime(invocationContext); + final long timeout = getHookRemainingTime(invocationContext); final OptableAttributes attributes = optableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), properties.getTimeout()); @@ -67,19 +75,15 @@ public Future> call(AuctionRequestPayloa timeout); if (targetingResultFuture == null) { - return failedFeature(this::sanitizePayload, moduleContext); + return failure( + this::sanitizePayload, + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure())); } - return targetingResultFuture.compose(targetingResult -> enrichedPayloadFuture(targetingResult, moduleContext)) - .recover(throwable -> failedFeature(this::sanitizePayload, moduleContext)); - } - - private ModuleContext createModuleContext() { - final ModuleContext moduleContext = new ModuleContext(); - moduleContext.setMetrics(Metrics.builder().moduleStartTime(System.currentTimeMillis()) - .build()); - - return moduleContext; + return targetingResultFuture.compose(targetingResult -> enrichedPayload(targetingResult, moduleContext)) + .recover(throwable -> failure( + this::sanitizePayload, + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()))); } private BidRequest getBidRequest(AuctionRequestPayload auctionRequestPayload) { @@ -88,30 +92,29 @@ private BidRequest getBidRequest(AuctionRequestPayload auctionRequestPayload) { .orElse(null); } - private long getHookRemainTime(AuctionInvocationContext invocationContext) { + private long getHookRemainingTime(AuctionInvocationContext invocationContext) { return Optional.ofNullable(invocationContext) .map(AuctionInvocationContext::timeout) .map(Timeout::remaining) .orElse(DEFAULT_API_CALL_TIMEOUT); } - private Future> enrichedPayloadFuture(TargetingResult targetingResult, - ModuleContext moduleContext) { + private Future> enrichedPayload(TargetingResult targetingResult, + ModuleContext moduleContext) { - moduleContext.setMetrics(moduleContext.getMetrics().toBuilder() - .moduleFinishTime(System.currentTimeMillis()) - .build()); + moduleContext.setFinishTime(System.currentTimeMillis()); if (targetingResult != null) { - moduleContext.setTargeting(targetingResult.getAudience()); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + moduleContext.setTargeting(targetingResult.getAudience()) + .setEnrichRequestStatus(EnrichmentStatus.success()); - return updateFeature(payload -> { + return update(payload -> { final AuctionRequestPayload sanitizedPayload = sanitizePayload(payload); return enrichPayload(sanitizedPayload, targetingResult); }, moduleContext); } else { - return failedFeature(this::sanitizePayload, moduleContext); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + return failure(this::sanitizePayload, moduleContext); } } @@ -123,7 +126,7 @@ private AuctionRequestPayload sanitizePayload(AuctionRequestPayload payload) { return AuctionRequestPayloadImpl.of(payloadResolver.clearBidRequest(payload.bidRequest())); } - private Future> updateFeature( + private static Future> update( Function func, ModuleContext moduleContext) { @@ -136,17 +139,16 @@ private Future> updateFeature( .build()); } - private Future> failedFeature(ModuleContext moduleContext) { - moduleContext.setEnrichRequestStatus(EnrichmentStatus.fail()); - return succeededFeature(moduleContext); + private static Future> failure(ModuleContext moduleContext) { + return success(moduleContext + .setEnrichRequestStatus(EnrichmentStatus.failure()) + .setFinishTime(System.currentTimeMillis())); } - private Future> failedFeature( + private static Future> failure( Function func, ModuleContext moduleContext) { - moduleContext.setEnrichRequestStatus(EnrichmentStatus.fail()); - return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) @@ -156,11 +158,7 @@ private Future> failedFeature( .build()); } - private Future> succeededFeature(ModuleContext moduleContext) { - moduleContext.setMetrics(moduleContext.getMetrics().toBuilder() - .moduleFinishTime(System.currentTimeMillis()) - .build()); - + private static Future> success(ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) @@ -168,4 +166,9 @@ private Future> succeededFeature(ModuleC .moduleContext(moduleContext) .build()); } + + @Override + public String code() { + return CODE; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java index 34f6e086e70..e95c14b3293 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java @@ -1,16 +1,20 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; import com.fasterxml.jackson.databind.ObjectMapper; -import lombok.AllArgsConstructor; import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.v1.analytics.Tags; -@AllArgsConstructor +import java.util.Objects; + public class AnalyticTagsResolver { private final ObjectMapper objectMapper; + public AnalyticTagsResolver(ObjectMapper objectMapper) { + this.objectMapper = Objects.requireNonNull(objectMapper); + } + public Tags resolve(ModuleContext moduleContext) { final Metrics metrics = moduleContext.getMetrics(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java index 7977a0434de..93a01eb5452 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Builder; import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; @@ -8,6 +9,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; import org.prebid.server.hooks.v1.analytics.Tags; import java.util.ArrayList; @@ -47,22 +49,39 @@ public Tags buildTags() { } private Activity buildEnrichRequestActivity() { - return ActivityImpl.of(ACTIVITY_ENRICH_REQUEST, - requestEnrichmentStatus != null - ? requestEnrichmentStatus.getValue() - : null, - List.of(ResultImpl.of(null, objectMapper.createObjectNode().put(STATUS_EXECUTION_TIME, - requestEnrichmentExecutionTime), null))); + final String activityStatus = requestEnrichmentStatus != null ? requestEnrichmentStatus.getValue() : null; + final Result activityResult = buildResult(STATUS_EXECUTION_TIME, requestEnrichmentExecutionTime); + + return ActivityImpl.of( + ACTIVITY_ENRICH_REQUEST, + activityStatus, + List.of(activityResult)); } private Activity buildEnrichResponseActivity() { - return ActivityImpl.of(ACTIVITY_ENRICH_RESPONSE, - responseEnrichmentStatus != null - ? responseEnrichmentStatus.status().getValue() - : null, - List.of(ResultImpl.of(null, objectMapper.createObjectNode().put(STATUS_REASON, - responseEnrichmentStatus != null - ? responseEnrichmentStatus.reason().getValue() - : null), null))); + final String activityStatus = requestEnrichmentStatus != null ? requestEnrichmentStatus.getValue() : null; + final String enrichmentStatus = responseEnrichmentStatus != null + ? responseEnrichmentStatus.reason().getValue() + : null; + final Result activityResult = buildResult(STATUS_REASON, enrichmentStatus); + + return ActivityImpl.of( + ACTIVITY_ENRICH_RESPONSE, + activityStatus, + List.of(activityResult)); + } + + private Result buildResult(String resultName, String resultValue) { + final ObjectNode resultObj = objectMapper.createObjectNode() + .put(resultName, resultValue); + + return ResultImpl.of(null, resultObj, null); + } + + private Result buildResult(String resultName, Long resultValue) { + final ObjectNode resultObj = objectMapper.createObjectNode() + .put(resultName, resultValue); + + return ResultImpl.of(null, resultObj, null); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java index d45de6f0c23..660d0429017 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java @@ -13,7 +13,10 @@ public class AuctionResponseValidator { - public EnrichmentStatus checkEnrichmentPossibility(BidResponse bidResponse, List targeting) { + private AuctionResponseValidator() { + } + + public static EnrichmentStatus checkEnrichmentPossibility(BidResponse bidResponse, List targeting) { Status status = Status.SUCCESS; Reason reason = Reason.NONE; @@ -31,7 +34,7 @@ public EnrichmentStatus checkEnrichmentPossibility(BidResponse bidResponse, List .build(); } - private boolean hasKeywords(List targeting) { + private static boolean hasKeywords(List targeting) { if (CollectionUtils.isEmpty(targeting)) { return false; } @@ -43,7 +46,7 @@ private boolean hasKeywords(List targeting) { return idsCounter > 0; } - private boolean hasBids(BidResponse bidResponse) { + private static boolean hasBids(BidResponse bidResponse) { final List seatBids = Optional.ofNullable(bidResponse).map(BidResponse::getSeatbid).orElse(null); if (CollectionUtils.isEmpty(seatBids)) { return false; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index 8a08414aff1..19f620ea9c6 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -4,7 +4,6 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Uid; -import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.tuple.Pair; import org.prebid.server.hooks.modules.optable.targeting.model.Id; @@ -16,13 +15,17 @@ import java.util.Objects; import java.util.stream.Collectors; -@AllArgsConstructor public class IdsMapper { private final ObjectMapper objectMapper; private final Map ppidMapping; + public IdsMapper(ObjectMapper objectMapper, Map ppidMapping) { + this.objectMapper = Objects.requireNonNull(objectMapper); + this.ppidMapping = Objects.requireNonNull(ppidMapping); + } + public List toIds(BidRequest bidRequest) { final IdsResolver idsResolver = IdsResolver.of(objectMapper, bidRequest); @@ -43,15 +46,20 @@ private Map toIds(List eids) { return null; } - return eids.stream().map(it -> { - final String key = ppidMapping.get(it.getSource()); + return eids.stream() + .map(this::eidSourceToFirstUidId) + .filter(Objects::nonNull) + .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + } + + private Pair eidSourceToFirstUidId(Eid eid) { + final String key = ppidMapping.get(eid.getSource()); - if (key != null) { - return Pair.of(key, it.getUids().stream().findFirst().map(Uid::getId).orElse(null)); - } + return key != null ? Pair.of(key, getFirstUidId(eid)) : null; + } - return null; - }).filter(Objects::nonNull).collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + private String getFirstUidId(Eid eid) { + return eid.getUids().stream().findFirst().map(Uid::getId).orElse(null); } private Map applyDynamicMapping(IdsResolver idsResolver) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java index c2852104f6c..4559cf8b1e1 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java @@ -106,7 +106,7 @@ private String getUid(String source) { } private Optional getExtUserOptable() { - return this.extUser.map(it -> it.getProperty("optable")) + return extUser.map(it -> it.getProperty("optable")) .map(it -> { try { return objectMapper.treeToValue(it, ExtUserOptable.class); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java index 4a6eee8f90c..6be859d0253 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java @@ -11,7 +11,10 @@ public class IpResolver { - public List resolveIp(AuctionContext auctionContext) { + private IpResolver() { + } + + public static List resolveIp(AuctionContext auctionContext) { final List result = new ArrayList<>(); final Optional auctionContextOpt = Optional.ofNullable(auctionContext); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 243ed1c5303..93676d9f538 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,32 +1,29 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.iab.gpp.encoder.GppModel; -import lombok.AllArgsConstructor; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.PrivacyContext; +import java.util.Collections; import java.util.List; import java.util.Optional; import java.util.Set; -@AllArgsConstructor public class OptableAttributesResolver { - private final IpResolver ipResolver; - public OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { - final List ips = ipResolver.resolveIp(auctionContext); - - OptableAttributes optableAttributes = getGdprPrivacyAttributes(auctionContext); - if (optableAttributes == null) { - optableAttributes = getGppPrivacyAttributes(auctionContext); - } - - return optableAttributes != null - ? optableAttributes.toBuilder().ips(ips).timeout(timeout).build() - : OptableAttributes.builder().ips(ips).timeout(timeout).build(); + final List ips = IpResolver.resolveIp(auctionContext); + + return Optional.ofNullable(getGdprPrivacyAttributes(auctionContext)) + .or(() -> Optional.ofNullable(getGppPrivacyAttributes(auctionContext))) + .map(OptableAttributes::toBuilder) + .orElseGet(OptableAttributes::builder) + .ips(ips) + .timeout(timeout) + .build(); } private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) { @@ -40,32 +37,27 @@ private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) .map(GppModel::encode) .orElse(null); - if (gppConsent != null) { - final Set sids = gppContextOpt - .map(GppContext::scope) - .map(GppContext.Scope::getSectionsIds) - .orElse(Set.of()); - - return OptableAttributes.builder().gpp(gppConsent).gppSid(sids).build(); - + if (gppConsent == null) { + return null; } - return null; + final Set sids = gppContextOpt + .map(GppContext::scope) + .map(GppContext.Scope::getSectionsIds) + .orElse(Collections.emptySet()); + + return OptableAttributes.builder().gpp(gppConsent).gppSid(sids).build(); } private OptableAttributes getGdprPrivacyAttributes(AuctionContext auctionContext) { return Optional.ofNullable(auctionContext) - .map(AuctionContext::getPrivacyContext) + .map(AuctionContext::getPrivacyContext) .map(PrivacyContext::getTcfContext) - .map(ctx -> { - - if (ctx.isConsentValid()) { - return OptableAttributes.builder() - .gdprConsent(ctx.getConsentString()) - .gdprApplies(ctx.isInGdprScope()) - .build(); - } - return null; - }).orElse(null); + .filter(TcfContext::isConsentValid) + .map(ctx -> OptableAttributes.builder() + .gdprConsent(ctx.getConsentString()) + .gdprApplies(ctx.isInGdprScope()) + .build()) + .orElse(null); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 203dccb3dcf..39cefaea3b2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -2,16 +2,21 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; -import lombok.AllArgsConstructor; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import java.util.Objects; import java.util.Optional; -@AllArgsConstructor public class OptableTargeting { + public OptableTargeting(IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient) { + this.idsMapper = Objects.requireNonNull(idsMapper); + this.queryBuilder = Objects.requireNonNull(queryBuilder); + this.apiClient = Objects.requireNonNull(apiClient); + } + private final IdsMapper idsMapper; private final QueryBuilder queryBuilder; private final APIClient apiClient; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java index fb33574f10a..1eb734ab25e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java @@ -3,28 +3,28 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.response.BidResponse; -import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidRequestBuilder; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidResponseBuilder; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidResponseResolver; import java.util.List; +import java.util.Objects; import java.util.Optional; -@AllArgsConstructor public class PayloadResolver { private ObjectMapper mapper; + public PayloadResolver(ObjectMapper mapper) { + this.mapper = Objects.requireNonNull(mapper); + } + public BidRequest enrichBidRequest(BidRequest bidRequest, TargetingResult targetingResults) { - if (bidRequest == null) { - return null; - } - if (targetingResults == null) { + if (bidRequest == null || targetingResults == null) { return bidRequest; } @@ -33,36 +33,27 @@ public BidRequest enrichBidRequest(BidRequest bidRequest, TargetingResult target return bidRequest; } - return BidRequestBuilder.of(bidRequest) + return new BidRequestBuilder(bidRequest) .addEids(user.getEids()) .addData(user.getData()) .build(); } public BidResponse enrichBidResponse(BidResponse bidResponse, List targeting) { - if (bidResponse == null) { - return null; - } - if (CollectionUtils.isEmpty(targeting)) { + if (bidResponse == null || CollectionUtils.isEmpty(targeting)) { return bidResponse; } - return BidResponseBuilder.of(bidResponse, mapper) - .applyTargeting(targeting) - .build(); + return BidResponseResolver.of(bidResponse, mapper).applyTargeting(targeting); } public BidRequest clearBidRequest(BidRequest bidRequest) { - if (bidRequest == null) { - return null; - } - - return BidRequestBuilder.of(bidRequest) - .clearExtUserOptable() - .build(); + return bidRequest != null + ? new BidRequestBuilder(bidRequest).clearExtUserOptable().build() + : null; } - private User getUser(TargetingResult targetingResults) { + private static User getUser(TargetingResult targetingResults) { return Optional.ofNullable(targetingResults) .map(TargetingResult::getOrtb2) .map(Ortb2::getUser) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 5ea1ad75027..44e63999ef0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -1,8 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; -import org.codehaus.plexus.util.StringUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; @@ -12,14 +11,16 @@ import java.util.Comparator; import java.util.List; import java.util.Optional; -import java.util.stream.IntStream; import java.util.stream.Stream; -@AllArgsConstructor public class QueryBuilder { private String idPrefixOrder; + public QueryBuilder(String idPrefixOrder) { + this.idPrefixOrder = idPrefixOrder; + } + public String build(List ids, OptableAttributes optableAttributes) { if (CollectionUtils.isEmpty(ids)) { return null; @@ -40,19 +41,22 @@ private List reorderIds(List ids) { final int lastIndex = ids.size() - 1; final List order = Stream.of(idPrefixOrder.split(",", -1)).toList(); final List orderedIds = new ArrayList<>(ids); - orderedIds.sort(Comparator.comparing(item -> { - int value = order.indexOf(item.getName()); - if (value == -1) { - value = lastIndex; - } - return value; - })); + + orderedIds.sort(Comparator.comparing(item -> checkOrder(item, order, lastIndex))); return orderedIds; } return ids; } + private int checkOrder(Id item, List order, int lastIndex) { + int value = order.indexOf(item.getName()); + if (value == -1) { + value = lastIndex; + } + return value; + } + private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes) { Optional.ofNullable(optableAttributes.getGdprConsent()).ifPresent(consent -> sb.append("&gdpr_consent=").append(consent)); @@ -71,15 +75,14 @@ private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes private void buildQueryString(StringBuilder sb, List ids) { final int size = ids.size(); - IntStream.range(0, size) - .forEach(index -> { - final Id id = ids.get(index); - sb.append(URLEncoder.encode( - "%s:%s".formatted(id.getName(), id.getValue()), - StandardCharsets.UTF_8)); - if (index != size - 1) { - sb.append("&id="); - } - }); + for (int index = 0; index < size; index++) { + final Id id = ids.get(index); + sb.append(URLEncoder.encode( + "%s:%s".formatted(id.getName(), id.getValue()), + StandardCharsets.UTF_8)); + if (index != size - 1) { + sb.append("&id="); + } + } } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java index 6280afbcff5..0ae7e050640 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java @@ -5,25 +5,24 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.User; -import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; -@AllArgsConstructor(staticName = "of") public class BidRequestBuilder { - BidRequest bidRequest; - - private static final EidsMerger EIDS_MERGER = new EidsMerger(); + private static final PayloadCleaner PAYLOAD_CLEANER = new PayloadCleaner(); - private static final DataMerger DATA_MERGER = new DataMerger(); + private BidRequest bidRequest; - private static final PayloadCleaner PAYLOAD_CLEANER = new PayloadCleaner(); + public BidRequestBuilder(BidRequest bidRequest) { + this.bidRequest = Objects.requireNonNull(bidRequest); + } public BidRequestBuilder addEids(List eids) { if (bidRequest == null || CollectionUtils.isEmpty(eids)) { @@ -33,7 +32,7 @@ public BidRequestBuilder addEids(List eids) { final User user = getOrCreateUser(bidRequest); bidRequest = bidRequest.toBuilder() .user(user.toBuilder() - .eids(EIDS_MERGER.merge(user.getEids(), eids)) + .eids(EidsMerger.merge(user.getEids(), eids)) .build()) .build(); @@ -49,7 +48,7 @@ public BidRequestBuilder addData(List data) { bidRequest = bidRequest.toBuilder() .user(user.toBuilder() - .data(DATA_MERGER.merge(user.getData(), data)) + .data(DataMerger.merge(user.getData(), data)) .build()) .build(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java similarity index 51% rename from extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java rename to extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java index abf8c2186f7..85933b58a36 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java @@ -17,68 +17,66 @@ import java.util.Optional; @AllArgsConstructor(staticName = "of") -public class BidResponseBuilder { - - private static final BaseMerger EXT_MERGER = new BaseMerger(); +public class BidResponseResolver { private BidResponse bidResponse; private ObjectMapper mapper; - public BidResponseBuilder applyTargeting(List targeting) { + public BidResponse applyTargeting(List targeting) { if (CollectionUtils.isEmpty(targeting)) { - return this; + return bidResponse; } final ObjectNode node = targetingToObjectNode(targeting); if (node == null) { - return this; + return bidResponse; } final List seatBids = Optional.ofNullable(bidResponse.getSeatbid()) .orElse(Collections.emptyList()) .stream().map(seatBid -> { - final List bids = Optional.ofNullable(seatBid.getBid()).orElse(Collections.emptyList()) - .stream().map(bid -> { - final ObjectNode extNode = Optional.ofNullable(bid.getExt()) - .orElseGet(() -> mapper.createObjectNode()); - final ObjectNode prebidNode = Optional.ofNullable((ObjectNode) (extNode.get("prebid"))) - .orElseGet(() -> mapper.createObjectNode()); - final ObjectNode targetingNode = - Optional.ofNullable((ObjectNode) (prebidNode.get("targeting"))) - .orElseGet(() -> mapper.createObjectNode()); - final JsonNode mergedTargetingNode = EXT_MERGER.mergeExt(targetingNode, node); - - prebidNode.set("targeting", mergedTargetingNode); - extNode.set("prebid", prebidNode); - - return bid.toBuilder().ext(extNode).build(); - }).toList(); + final List bids = Optional.ofNullable(seatBid.getBid()) + .orElse(Collections.emptyList()) + .stream() + .map(bid -> applyTargeting(bid, node)) + .toList(); return seatBid.toBuilder().bid(bids).build(); }).toList(); - bidResponse = bidResponse.toBuilder() + return bidResponse.toBuilder() .seatbid(seatBids) .build(); + } + + private Bid applyTargeting(Bid bid, ObjectNode node) { + final ObjectNode extNode = getOrCreateNode(bid.getExt()); + final ObjectNode prebidNode = getOrCreateNode((ObjectNode) extNode.get("prebid")); + final ObjectNode targetingNode = getOrCreateNode((ObjectNode) prebidNode.get("targeting")); + final JsonNode mergedTargetingNode = ExtMerger.mergeExt(targetingNode, node); + + prebidNode.set("targeting", mergedTargetingNode); + extNode.set("prebid", prebidNode); + + return bid.toBuilder().ext(extNode).build(); + } - return this; + private ObjectNode getOrCreateNode(ObjectNode node) { + return Optional.ofNullable(node).orElseGet(() -> mapper.createObjectNode()); } private ObjectNode targetingToObjectNode(List targeting) { final ObjectNode node = mapper.createObjectNode(); - targeting.forEach(it -> { - final List ids = it.getIds(); + + for (Audience audience: targeting) { + final List ids = audience.getIds(); if (CollectionUtils.isNotEmpty(ids)) { final List strIds = ids.stream().map(AudienceId::getId).toList(); - node.putIfAbsent(it.getKeyspace(), TextNode.valueOf(String.join(",", strIds))); + node.putIfAbsent(audience.getKeyspace(), TextNode.valueOf(String.join(",", strIds))); } - }); + } return node; } - - public BidResponse build() { - return bidResponse; - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java index 57452f3b3f2..399cd02e5c6 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java @@ -9,16 +9,21 @@ import java.util.Optional; import java.util.stream.Collectors; -public class DataMerger extends BaseMerger { +public class DataMerger { + + private DataMerger() { + } + + public static List merge(List destination, + List source) { - public List merge(List destination, List source) { if (CollectionUtils.isEmpty(source)) { return destination; } final Map idToData = mapDataToId(destination); if (idToData == null || idToData.isEmpty()) { - return source.stream().map(this::toData).toList(); + return source.stream().map(DataMerger::toData).toList(); } source.forEach(data -> idToData.compute(data.getId(), (id, item) -> item != null @@ -28,7 +33,7 @@ public List merge(List mapSegmentToId(List segment) { + private static Map mapSegmentToId(List segment) { return CollectionUtils.isNotEmpty(segment) ? segment.stream().collect(Collectors.toMap(Segment::getId, it -> it)) : null; } - private com.iab.openrtb.request.Data toData(Data data) { + private static com.iab.openrtb.request.Data toData(Data data) { if (data == null) { return null; } @@ -93,7 +98,7 @@ private com.iab.openrtb.request.Data toData(Data data) { .build(); } - private Map mapDataToId(List data) { + private static Map mapDataToId(List data) { return CollectionUtils.isNotEmpty(data) ? data.stream().collect(Collectors.toMap(com.iab.openrtb.request.Data::getId, it -> it)) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java index 805635c469c..a3e8f85c1a1 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java @@ -9,9 +9,12 @@ import java.util.Optional; import java.util.stream.Collectors; -public class EidsMerger extends BaseMerger { +public class EidsMerger { - public List merge(List destination, List source) { + private EidsMerger() { + } + + public static List merge(List destination, List source) { if (CollectionUtils.isEmpty(source)) { return destination; } @@ -29,7 +32,7 @@ public List merge(List destination, List source) { return sourceToEid.values().stream().toList(); } - private Eid mergeEids(Eid destination, Eid source) { + private static Eid mergeEids(Eid destination, Eid source) { if (source == null) { return destination; } @@ -50,20 +53,20 @@ private Eid mergeEids(Eid destination, Eid source) { .build(); } - private Uid mergeUids(Uid destination, Uid source) { + private static Uid mergeUids(Uid destination, Uid source) { return destination.toBuilder() .atype(source.getAtype()) - .ext(mergeExt(destination.getExt(), source.getExt())) + .ext(ExtMerger.mergeExt(destination.getExt(), source.getExt())) .build(); } - private Map mapEidToSource(List eids) { + private static Map mapEidToSource(List eids) { return CollectionUtils.isNotEmpty(eids) ? eids.stream().collect(Collectors.toMap(Eid::getSource, eid -> eid)) : null; } - private Map mapUidToId(List uids) { + private static Map mapUidToId(List uids) { return CollectionUtils.isNotEmpty(uids) ? uids.stream().collect(Collectors.toMap(Uid::getId, uid -> uid)) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java similarity index 77% rename from extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java rename to extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java index 929ae8fe847..09d1cfd01bd 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMerger.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java @@ -4,9 +4,12 @@ import java.util.Optional; -public class BaseMerger { +public class ExtMerger { - protected ObjectNode mergeExt(ObjectNode origin, ObjectNode newExt) { + private ExtMerger() { + } + + protected static ObjectNode mergeExt(ObjectNode origin, ObjectNode newExt) { if (newExt == null) { return origin; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index 703af7399f1..ff4453c35be 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -6,7 +6,6 @@ import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; import io.vertx.core.http.impl.headers.HeadersMultiMap; -import lombok.AllArgsConstructor; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; @@ -22,10 +21,10 @@ import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.TimeoutException; -@AllArgsConstructor public class APIClient { private static final Logger logger = LoggerFactory.getLogger(APIClient.class); @@ -42,6 +41,19 @@ public class APIClient { private final String apiKey; + public APIClient(String endpoint, + HttpClient httpClient, + double logSamplingRate, + OptableResponseParser responseParser, + String apiKey) { + + this.endpoint = Objects.requireNonNull(endpoint); + this.httpClient = Objects.requireNonNull(httpClient); + this.logSamplingRate = logSamplingRate; + this.responseParser = Objects.requireNonNull(responseParser); + this.apiKey = apiKey; + } + public Future getTargeting(String query, List ips, long timeout) { logger.debug("Query string: %s".formatted(query)); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index a521090165b..9e2df353898 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -46,9 +46,10 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { @BeforeEach public void setUp() { payloadResolver = new PayloadResolver(mapper); - auctionResponseValidator = new AuctionResponseValidator(); - target = new OptableTargetingAuctionResponseHook(new AnalyticTagsResolver(mapper), - payloadResolver, true, auctionResponseValidator); + target = new OptableTargetingAuctionResponseHook( + new AnalyticTagsResolver(mapper), + payloadResolver, + true); } @Test @@ -122,8 +123,10 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", List.of(new AudienceId("audienceId")), "keyspace", 1)))); - target = new OptableTargetingAuctionResponseHook(new AnalyticTagsResolver(mapper), - payloadResolver, false, auctionResponseValidator); + target = new OptableTargetingAuctionResponseHook( + new AnalyticTagsResolver(mapper), + payloadResolver, + false); // when final Future> future = target.call(auctionResponsePayload, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index f621b1ee7c2..5f2beebd00e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -1,6 +1,14 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; 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.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.Module; @@ -10,8 +18,24 @@ import static org.assertj.core.api.Assertions.assertThat; +@ExtendWith(MockitoExtension.class) public class OptableTargetingModuleTest { + @Mock + OptableTargetingProperties properties; + + @Mock + OptableTargeting optableTargeting; + + @Mock + PayloadResolver payloadResolver; + + @Mock + OptableAttributesResolver optableAttributesResolver; + + @Mock + AnalyticTagsResolver analyticTagsResolver; + @Test public void shouldReturnNonBlankCode() { // given @@ -28,8 +52,15 @@ public void shouldReturnNonBlankCode() { public void shouldReturnHooks() { // given final Collection> hooks = - List.of(new OptableTargetingProcessedAuctionRequestHook(null, null, null, null), - new OptableTargetingAuctionResponseHook(null, null, true, null)); + List.of(new OptableTargetingProcessedAuctionRequestHook( + properties, + optableTargeting, + payloadResolver, + optableAttributesResolver), + new OptableTargetingAuctionResponseHook( + analyticTagsResolver, + payloadResolver, + true)); final Module module = new OptableTargetingModule(hooks); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 0e79998bd95..cb0d85421a3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -14,15 +14,15 @@ import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.IpResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.json.ObjectMapperProvider; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -52,15 +52,13 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable private OptableTargetingProcessedAuctionRequestHook target; - private IpResolver ipResolver; - @BeforeEach public void setUp() { when(timeout.remaining()).thenReturn(1000L); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(timeout)); + mapper = ObjectMapperProvider.mapper(); payloadResolver = new PayloadResolver(mapper); - ipResolver = new IpResolver(); - optableAttributesResolver = new OptableAttributesResolver(ipResolver); + optableAttributesResolver = new OptableAttributesResolver(); target = new OptableTargetingProcessedAuctionRequestHook(optableTargetingProperties, optableTargeting, payloadResolver, optableAttributesResolver); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java index 03049834fc6..40a6cfd9e75 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java @@ -3,7 +3,6 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.Reason; @@ -17,20 +16,15 @@ public class AuctionResponseValidatorTest { - private AuctionResponseValidator target; - - @BeforeEach - public void setUp() { - target = new AuctionResponseValidator(); - } - @Test public void shouldReturnNobidStatusWhenBidResponseIsEmpty() { // given final BidResponse bidResponse = BidResponse.builder().build(); // when - final EnrichmentStatus result = target.checkEnrichmentPossibility(bidResponse, givenTargeting()); + final EnrichmentStatus result = AuctionResponseValidator.checkEnrichmentPossibility( + bidResponse, + givenTargeting()); // then assertThat(result).isNotNull() @@ -44,7 +38,9 @@ public void shouldReturnNoKeywordsStatusWhenTargetingHasNoIds() { final BidResponse bidResponse = BidResponse.builder().build(); // when - final EnrichmentStatus result = target.checkEnrichmentPossibility(bidResponse, givenTargeting()); + final EnrichmentStatus result = AuctionResponseValidator.checkEnrichmentPossibility( + bidResponse, + givenTargeting()); // then assertThat(result).isNotNull() @@ -62,7 +58,9 @@ public void shouldReturnSuccessStatus() { .build(); // when - final EnrichmentStatus result = target.checkEnrichmentPossibility(bidResponse, givenTargeting()); + final EnrichmentStatus result = AuctionResponseValidator.checkEnrichmentPossibility( + bidResponse, + givenTargeting()); // then assertThat(result).isNotNull() diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index ed51ed970d7..7eaa8d04e9b 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -44,7 +44,7 @@ public class OptableAttributesResolverTest extends BaseOptableTest { @BeforeEach public void setUp() { when(properties.getTimeout()).thenReturn(100L); - target = new OptableAttributesResolver(new IpResolver()); + target = new OptableAttributesResolver(); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java index d2b7937b394..b21267f0d72 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; @@ -13,13 +12,6 @@ public class DataMergerTest extends BaseMergerTest { - private DataMerger target; - - @BeforeEach - public void setUp() { - target = new DataMerger(); - } - @Test public void shouldMergeDifferentData() { // given @@ -28,7 +20,7 @@ public void shouldMergeDifferentData() { final List source = givenOptableData("dataId2", "segmentId2", "field2", "value2"); // when - final List result = target.merge(destination, source); + final List result = DataMerger.merge(destination, source); // then assertThat(result).isNotNull() @@ -53,7 +45,7 @@ public void shouldMergeSegmentsWithinTheSameData() { final List source = givenOptableData("dataId1", "segmentId2", "field2", "value2"); // when - final List result = target.merge(destination, source); + final List result = DataMerger.merge(destination, source); // then assertThat(result).isNotNull() @@ -75,7 +67,7 @@ public void shouldMergeExtWithinTheSameSegment() { final List source = givenOptableData("dataId1", "segmentId1", "field2", "value2"); // when - final List result = target.merge(destination, source); + final List result = DataMerger.merge(destination, source); // then assertThat(result).isNotNull() @@ -94,7 +86,7 @@ public void shouldUseFirstArgumentWhenSecondIsAbsent() { final List source = givenOptableData("dataId1", "segmentId1", "field1", "value1"); // when - final List result = target.merge(null, source); + final List result = DataMerger.merge(null, source); // then assertThat(result).isNotNull() @@ -112,7 +104,7 @@ public void shouldUseSecondArgumentWhenFirstIsAbsent() { final List destination = givenORTBData("dataId1", "segmentId1", "field1", "value1"); // when - final List result = target.merge(destination, null); + final List result = DataMerger.merge(destination, null); // then assertThat(result).isNotNull() @@ -127,7 +119,7 @@ public void shouldUseSecondArgumentWhenFirstIsAbsent() { @Test public void shouldNotFailWhenArgumentsAreAbsent() { // given and when - final List result = target.merge(null, null); + final List result = DataMerger.merge(null, null); // then assertThat(result).isNull(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java index 1396cdbf7b1..ca39ad458da 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java @@ -2,7 +2,6 @@ import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Uid; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.List; @@ -13,13 +12,6 @@ public class EidsMergerTest extends BaseMergerTest { - private EidsMerger target; - - @BeforeEach - public void setUp() { - target = new EidsMerger(); - } - @Test public void shouldMergeDifferentEids() { // given @@ -27,7 +19,7 @@ public void shouldMergeDifferentEids() { final List source = givenEids("source2", "uid2", "field2", "value2"); // when - final List result = target.merge(destination, source); + final List result = EidsMerger.merge(destination, source); // then assertThat(result).isNotNull() @@ -51,7 +43,7 @@ public void shouldMergeUidsWithinTheSameEid() { final List source = givenEids("source1", "uid2", "field2", "value2"); // when - final List result = target.merge(destination, source); + final List result = EidsMerger.merge(destination, source); // then assertThat(result).isNotNull() @@ -72,7 +64,7 @@ public void shouldMergeExtWithinTheSameUid() { final List source = givenEids("source1", "uid1", "field2", "value2"); // when - final List result = target.merge(destination, source); + final List result = EidsMerger.merge(destination, source); // then assertThat(result).isNotNull() @@ -91,7 +83,7 @@ public void shouldUserFirstUidsListWhenSecondIsAbsent() { final List destination = givenEids("source1", "uid1", "field1", "value1"); // when - final List result = target.merge(destination, null); + final List result = EidsMerger.merge(destination, null); // then assertThat(result).isNotNull() @@ -109,7 +101,7 @@ public void shouldUserSecondUidsListWhenSecondIsEmpty() { final List source = givenEids("source1", "uid1", "field1", "value1"); // when - final List result = target.merge(null, source); + final List result = EidsMerger.merge(null, source); // then assertThat(result).isNotNull() @@ -125,9 +117,9 @@ private List givenEids(String source, String uidId, String extField, String return List.of(Eid.builder() .source(source) .uids(List.of(Uid.builder() - .id(uidId) - .ext(givenExt(Map.of(extField, extValue))) - .build())) + .id(uidId) + .ext(givenExt(Map.of(extField, extValue))) + .build())) .build()); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java index beee6fd7f23..2d01c66d14f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java @@ -1,7 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; import com.fasterxml.jackson.databind.node.ObjectNode; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.util.Map; @@ -10,13 +9,6 @@ public class ExtMergerTest extends BaseMergerTest { - private BaseMerger target; - - @BeforeEach - public void setUp() { - target = new BaseMerger(); - } - @Test public void shouldMergeTwoExtObjects() { // given @@ -24,7 +16,7 @@ public void shouldMergeTwoExtObjects() { final ObjectNode source = givenExt(Map.of("field3", "value3", "field4", "value4")); // when - final ObjectNode result = target.mergeExt(destination, source); + final ObjectNode result = ExtMerger.mergeExt(destination, source); // then assertThat(result).isNotNull().hasSize(4); @@ -40,7 +32,7 @@ public void shouldUseFirstArgumentWhenSecondIsNull() { final ObjectNode destination = givenExt(Map.of("field1", "value1", "field2", "value2")); // when - final ObjectNode result = target.mergeExt(destination, null); + final ObjectNode result = ExtMerger.mergeExt(destination, null); // then assertThat(result).isNotNull().hasSize(2); @@ -54,7 +46,7 @@ public void shouldUseSecondArgumentWhenFirstIsNull() { final ObjectNode source = givenExt(Map.of("field1", "value1", "field2", "value2")); // when - final ObjectNode result = target.mergeExt(null, source); + final ObjectNode result = ExtMerger.mergeExt(null, source); // then assertThat(result).isNotNull().hasSize(2); @@ -65,7 +57,7 @@ public void shouldUseSecondArgumentWhenFirstIsNull() { @Test public void shouldNotFail() { // given and when - final ObjectNode result = target.mergeExt(null, null); + final ObjectNode result = ExtMerger.mergeExt(null, null); // then assertThat(result).isNull(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index 246235d1476..f6bea9eaf18 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; -import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpMethod; @@ -15,6 +14,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.vertx.httpclient.HttpClient; import java.util.List; @@ -43,7 +43,8 @@ public class APIClientTest extends BaseOptableTest { private APIClient target; - private final OptableResponseParser parser = new OptableResponseParser(new JacksonMapper(new ObjectMapper())); + private final OptableResponseParser parser = new OptableResponseParser( + new JacksonMapper(ObjectMapperProvider.mapper())); @BeforeEach public void setUp() { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java index 5dfae7ab124..a223ca3075b 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; @@ -10,6 +9,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.json.DecodeException; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -18,7 +18,7 @@ public class OptableResponseParserTest extends BaseOptableTest { private OptableResponseParser target; - private final JacksonMapper mapper = new JacksonMapper(new ObjectMapper()); + private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); @BeforeEach public void setUp() { diff --git a/sample/configs/prebid-config-optable.yaml b/sample/configs/prebid-config-with-optable.yaml similarity index 100% rename from sample/configs/prebid-config-optable.yaml rename to sample/configs/prebid-config-with-optable.yaml From 4c6708ba662b78902a520cca2e1673ebac959bc3 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 27 Mar 2025 10:54:52 +0100 Subject: [PATCH 07/41] optable-targeting: Code cleanup --- .../optable/targeting/v1/core/merger/BidRequestBuilder.java | 4 +--- .../optable/targeting/v1/core/merger/PayloadCleaner.java | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java index 0ae7e050640..cb9a9520a65 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java @@ -16,8 +16,6 @@ public class BidRequestBuilder { - private static final PayloadCleaner PAYLOAD_CLEANER = new PayloadCleaner(); - private BidRequest bidRequest; public BidRequestBuilder(BidRequest bidRequest) { @@ -78,7 +76,7 @@ public BidRequestBuilder clearExtUserOptable() { final JsonNode userExtOptable = userOpt.map(User::getExt) .map(it -> it.getProperty("optable")) - .map(it -> PAYLOAD_CLEANER.cleanUserExtOptable((ObjectNode) it)) + .map(it -> PayloadCleaner.cleanUserExtOptable((ObjectNode) it)) .orElse(null); if (userExtOptable != null) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java index 4a0e3605665..f4cbe3e0192 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java @@ -8,7 +8,7 @@ public class PayloadCleaner { private static final List FIELDS_FILTER = List.of("email", "phone", "zip", "vid"); - public ObjectNode cleanUserExtOptable(ObjectNode optable) { + public static ObjectNode cleanUserExtOptable(ObjectNode optable) { return optable.deepCopy().remove(FIELDS_FILTER); } } From a809dffa13535d279580464262a6c85e0cc3a87a Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 31 Mar 2025 12:03:58 +0200 Subject: [PATCH 08/41] optable-targeting: Remove logs --- .../hooks/modules/optable/targeting/v1/net/APIClient.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index ff4453c35be..db3e6543afa 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -55,8 +55,6 @@ public APIClient(String endpoint, } public Future getTargeting(String query, List ips, long timeout) { - logger.debug("Query string: %s".formatted(query)); - final MultiMap headers = HeadersMultiMap.headers() .add(HttpUtil.ACCEPT_HEADER, "application/json"); From 75fbd32e20c35c98ce5264e998e2f6628d06f2a1 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 10 Apr 2025 09:54:15 +0200 Subject: [PATCH 09/41] optable-targeting: Implement caching for targeting responses --- .../config/OptableTargetingConfig.java | 38 ++++- .../model/config/CacheProperties.java | 11 ++ .../config/OptableTargetingProperties.java | 3 + ...eTargetingProcessedAuctionRequestHook.java | 2 + .../optable/targeting/v1/core/Cache.java | 58 ++++++++ .../targeting/v1/core/OptableTargeting.java | 29 +++- .../optable/targeting/v1/net/APIClient.java | 8 +- ...Parser.java => OptableResponseMapper.java} | 12 +- .../optable/targeting/v1/core/CacheTest.java | 130 ++++++++++++++++++ .../v1/core/OptableTargetingTest.java | 84 ++++++++++- .../targeting/v1/net/APIClientTest.java | 2 +- ...st.java => OptableResponseMapperTest.java} | 8 +- 12 files changed, 365 insertions(+), 20 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java rename extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/{OptableResponseParser.java => OptableResponseMapper.java} (66%) create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java rename extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/{OptableResponseParserTest.java => OptableResponseMapperTest.java} (92%) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 2d6f55034e8..ce40749002c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -2,11 +2,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; +import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; @@ -14,7 +17,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseParser; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.spring.config.VertxContextScope; @@ -62,8 +65,8 @@ QueryBuilder queryBuilder(OptableTargetingProperties properties) { } @Bean - OptableResponseParser optableResponseParser(JacksonMapper mapper) { - return new OptableResponseParser(mapper); + OptableResponseMapper optableResponseParser(JacksonMapper mapper) { + return new OptableResponseMapper(mapper); } @Bean @@ -79,7 +82,7 @@ APIClient apiClient(OptableHttpClientWrapper httpClientWrapper, @Value("${logging.sampling-rate:0.01}") double logSamplingRate, OptableTargetingProperties properties, - OptableResponseParser responseParser) { + OptableResponseMapper responseParser) { final String endpoint = HttpUtil.validateUrl(Objects.requireNonNull(properties.getApiEndpoint())); @@ -92,11 +95,34 @@ OptableAttributesResolver optableAttributesResolver() { return new OptableAttributesResolver(); } + @Bean + Cache cache(PbcStorageService cacheService, + OptableResponseMapper responseParser, + OptableTargetingProperties properties, + @Value("${storage.pbc.enabled:false}") + boolean storageEnabled, + @Value("${cache.module.enabled:false}") + boolean moduleCacheEnabled + ) { + + final CacheProperties cacheProperties = properties.getCache(); + final boolean isCacheEnabled = cacheProperties.isEnabled(); + final boolean isCacheStorageEnabled = storageEnabled && moduleCacheEnabled; + + return new Cache( + cacheService, + responseParser, + isCacheEnabled && isCacheStorageEnabled, + cacheProperties.getTtlseconds()); + } + @Bean OptableTargeting optableTargeting(IdsMapper parametersExtractor, - QueryBuilder queryBuilder, APIClient apiClient) { + QueryBuilder queryBuilder, + APIClient apiClient, + Cache cache) { - return new OptableTargeting(parametersExtractor, queryBuilder, apiClient); + return new OptableTargeting(cache, parametersExtractor, queryBuilder, apiClient); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java new file mode 100644 index 00000000000..cca7dada5b1 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.optable.targeting.model.config; + +import lombok.Data; + +@Data +public class CacheProperties { + + boolean enabled = false; + + int ttlseconds = 86400; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index bce2f4ec1f9..9df32381f7b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.model.config; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -21,4 +22,6 @@ public final class OptableTargetingProperties { Long timeout; String idPrefixOrder; + + CacheProperties cache = new CacheProperties(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 1ba41286154..4556ed9965f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -2,6 +2,7 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import org.prebid.server.cache.PbcStorageService; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -11,6 +12,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java new file mode 100644 index 00000000000..6b8edd26b60 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -0,0 +1,58 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import io.vertx.core.Future; +import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.cache.proto.request.module.StorageDataType; +import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; + +import java.util.Objects; +import java.util.function.Function; + +public class Cache { + + private static final String APP_CODE = "prebid-Java"; + + private static final String APPLICATION = "optable-targeting"; + + private final PbcStorageService cacheService; + + private final OptableResponseMapper optableResponseMapper; + + private final boolean isEnabled; + + private final int ttlSeconds; + + public Cache(PbcStorageService cacheService, + OptableResponseMapper optableResponseMapper, + boolean isEnabled, + int ttlSeconds) { + + this.cacheService = Objects.requireNonNull(cacheService); + this.optableResponseMapper = Objects.requireNonNull(optableResponseMapper); + this.isEnabled = isEnabled; + this.ttlSeconds = ttlSeconds; + } + + public Future get(String query) { + return cacheService.retrieveEntry(query, APP_CODE, APPLICATION) + .map(ModuleCacheResponse::getValue) + .map(optableResponseMapper::parse) + .otherwise(it -> null); + } + + public boolean put(String query, TargetingResult value) { + cacheService.storeEntry( + query, + optableResponseMapper.toJsonString(value), + StorageDataType.TEXT, + ttlSeconds, + APPLICATION, APP_CODE); + return true; + } + + public boolean isEnabled() { + return isEnabled; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 39cefaea3b2..f1c94dc1cd7 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -1,17 +1,25 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.iab.openrtb.request.BidRequest; +import io.vertx.core.AsyncResult; import io.vertx.core.Future; +import io.vertx.core.Handler; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Objects; import java.util.Optional; public class OptableTargeting { - public OptableTargeting(IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient) { + private final Cache cache; + + public OptableTargeting(Cache cache, IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient) { + this.cache = cache; this.idsMapper = Objects.requireNonNull(idsMapper); this.queryBuilder = Objects.requireNonNull(queryBuilder); this.apiClient = Objects.requireNonNull(apiClient); @@ -25,7 +33,24 @@ public Future getTargeting(BidRequest bidRequest, OptableAttrib return Optional.ofNullable(bidRequest) .map(idsMapper::toIds) .map(ids -> queryBuilder.build(ids, attributes)) - .map(query -> apiClient.getTargeting(query, attributes.getIps(), timeout)) + .map(query -> cache.isEnabled() + ? getTargetingWithCache(query, attributes.getIps(), timeout) + : apiClient.getTargeting(query, attributes.getIps(), timeout) ) .orElse(Future.failedFuture("Can't get targeting")); } + + private Future getTargetingWithCache(String query, List ips, long timeout) { + final String key = URLEncoder.encode(query, StandardCharsets.UTF_8); + return cache.get(key) + .compose(it -> it == null + ? callAPIAndCacheResult(query, ips, timeout) + : Future.succeededFuture(it)) + .recover(throwable -> callAPIAndCacheResult(query, ips, timeout)); + } + + private Future callAPIAndCacheResult(String query, List ips, long timeout) { + return apiClient.getTargeting(query, ips, timeout) + .onComplete(targetingResultAsyncResult -> + cache.put(query, targetingResultAsyncResult.result())); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index db3e6543afa..294de7fcc40 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -37,20 +37,20 @@ public class APIClient { private final double logSamplingRate; - private final OptableResponseParser responseParser; + private final OptableResponseMapper responseMapper; private final String apiKey; public APIClient(String endpoint, HttpClient httpClient, double logSamplingRate, - OptableResponseParser responseParser, + OptableResponseMapper responseMapper, String apiKey) { this.endpoint = Objects.requireNonNull(endpoint); this.httpClient = Objects.requireNonNull(httpClient); this.logSamplingRate = logSamplingRate; - this.responseParser = Objects.requireNonNull(responseParser); + this.responseMapper = Objects.requireNonNull(responseMapper); this.apiKey = apiKey; } @@ -88,7 +88,7 @@ private Future doRequest(HttpRequest httpRequest, long timeout) private TargetingResult parseSilent(OptableCall optableCall, HttpRequest httpRequest) { try { - return responseParser.parse(optableCall); + return responseMapper.parse(optableCall); } catch (Exception e) { logParsingError(e, httpRequest); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java similarity index 66% rename from extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java rename to extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java index e9519cfcb91..27f0a7ccc9b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParser.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java @@ -8,7 +8,7 @@ import java.util.Optional; @AllArgsConstructor -public class OptableResponseParser { +public class OptableResponseMapper { private final JacksonMapper mapper; @@ -18,4 +18,14 @@ public TargetingResult parse(OptableCall call) { .map(resp -> mapper.decodeValue(resp.getBody(), TargetingResult.class)) .orElse(null); } + + public TargetingResult parse(String json) { + return Optional.ofNullable(json) + .map(it -> mapper.decodeValue(it, TargetingResult.class)) + .orElse(null); + } + + public String toJsonString(TargetingResult value) { + return mapper.encodeToString(value); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java new file mode 100644 index 00000000000..2fa1b2a193e --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -0,0 +1,130 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import io.vertx.core.Future; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.cache.proto.request.module.StorageDataType; +import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CacheTest { + + @Mock + private PbcStorageService pbcStorageService; + + private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); + private final OptableResponseMapper optableResponseMapper = spy(new OptableResponseMapper(mapper)); + + private Cache target; + + @BeforeEach + public void setUp() { + target = new Cache(pbcStorageService, optableResponseMapper, true, 86400); + } + + @Test + public void cacheShouldBeEnabled() { + // given and when + boolean isEnabled = target.isEnabled(); + + // then + Assertions.assertThat(isEnabled).isTrue(); + } + + @Test + public void cacheShouldBeDisabled() { + // given + target = new Cache(pbcStorageService, optableResponseMapper, false, 86400); + + // when + boolean isEnabled = target.isEnabled(); + + // then + Assertions.assertThat(isEnabled).isFalse(); + } + + @Test + public void cacheShouldNotCallMapperIfNoEntry() { + // given + when(pbcStorageService.retrieveEntry(any(), any(), any())).thenReturn( + Future.succeededFuture(ModuleCacheResponse.empty())); + + // when + TargetingResult result = target.get("key").result(); + + // then + Assertions.assertThat(result).isNull(); + verify(optableResponseMapper, times(0)).parse(anyString()); + } + + @Test + public void cacheShouldReturnEntry() { + // given + final TargetingResult targetingResult = givenTargetingResult(); + when(pbcStorageService.retrieveEntry(any(), any(), any())).thenReturn( + Future.succeededFuture(ModuleCacheResponse.of("key", StorageDataType.TEXT, + mapper.encodeToString(targetingResult)))); + + // when + TargetingResult result = target.get("key").result(); + + // then + Assertions.assertThat(result) + .isNotNull() + .isEqualTo(targetingResult); + + verify(optableResponseMapper, times(1)).parse(anyString()); + } + + @Test + public void cacheShouldStoreEntry() { + // given + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final boolean result = target.put("key", targetingResult); + + // then + Assertions.assertThat(result).isTrue(); + verify(pbcStorageService, times(1)).storeEntry( + eq("key"), + eq(mapper.encodeToString(targetingResult)), + eq(StorageDataType.TEXT), + eq(86400), + any(), + any()); + } + + private TargetingResult givenTargetingResult() { + return new TargetingResult( + List.of(new Audience( + "provider", + List.of(new AudienceId("1")), + "keyspace", + 0)), + new Ortb2(new User(null, null))); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index a176683bef3..204344eba14 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -20,12 +20,18 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class OptableTargetingTest extends BaseOptableTest { + @Mock(strictness = Mock.Strictness.LENIENT) + private Cache cache; + @Mock private IdsMapper idsMapper; @@ -40,13 +46,62 @@ public class OptableTargetingTest extends BaseOptableTest { @BeforeEach public void setUp() { + when(cache.isEnabled()).thenReturn(false); optableAttributes = givenOptableAttributes(); - target = new OptableTargeting(idsMapper, queryBuilder, apiClient); + target = new OptableTargeting(cache, idsMapper, queryBuilder, apiClient); + } + + @Test + public void shouldReturnTargetingResultsAndNotUseCache() { + // given + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache, times(0)).get(any()); + verify(apiClient, times(1)).getTargeting(any(), any(), anyLong()); + verify(cache, times(0)).put(any(), eq(targetingResult.result())); + } + + @Test + public void shouldCallAPIAndAddTargetingResultsToCache() { + // given + when(cache.isEnabled()).thenReturn(true); + when(cache.get(any())).thenReturn(Future.succeededFuture(null)); + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache).put(any(), eq(targetingResult.result())); } @Test - public void shouldReturnTargetingResults() { + public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() { // given + when(cache.isEnabled()).thenReturn(true); + when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); final BidRequest bidRequest = givenBidRequest(); when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); when(apiClient.getTargeting(any(), any(), anyLong())) @@ -62,6 +117,31 @@ public void shouldReturnTargetingResults() { .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(apiClient, times(1)).getTargeting(any(), any(), anyLong()); + verify(cache).put(any(), eq(targetingResult.result())); + } + + @Test + public void shouldUseCachedResult() { + // given + when(cache.isEnabled()).thenReturn(true); + when(cache.get(any())).thenReturn(Future.succeededFuture(givenTargetingResult())); + final BidRequest bidRequest = givenBidRequest(); + when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + + // when + final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache, times(1)).get(any()); + verify(apiClient, times(0)).getTargeting(any(), any(), anyLong()); + verify(cache, times(0)).put(any(), eq(targetingResult.result())); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index f6bea9eaf18..917343b1dfe 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -43,7 +43,7 @@ public class APIClientTest extends BaseOptableTest { private APIClient target; - private final OptableResponseParser parser = new OptableResponseParser( + private final OptableResponseMapper parser = new OptableResponseMapper( new JacksonMapper(ObjectMapperProvider.mapper())); @BeforeEach diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java similarity index 92% rename from extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java rename to extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java index a223ca3075b..c130a4c3af5 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseParserTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java @@ -14,22 +14,22 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertThrows; -public class OptableResponseParserTest extends BaseOptableTest { +public class OptableResponseMapperTest extends BaseOptableTest { - private OptableResponseParser target; + private OptableResponseMapper target; private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); @BeforeEach public void setUp() { - target = new OptableResponseParser(mapper); + target = new OptableResponseMapper(mapper); } @Test public void shouldNotFailWhenSourceIsNull() { // given and when - final TargetingResult result = target.parse(null); + final TargetingResult result = target.parse((OptableCall) null); //then assertThat(result).isNull(); From b669837477d1695347d4590b3e942f6c4ecebc91 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sat, 12 Apr 2025 23:55:39 +0200 Subject: [PATCH 10/41] optable-targeting: Add account-level configuration --- .../config/OptableTargetingConfig.java | 54 ++++++------- .../model/config/CacheProperties.java | 14 +++- .../config/OptableTargetingProperties.java | 23 +++++- .../OptableTargetingAuctionResponseHook.java | 11 ++- ...eTargetingProcessedAuctionRequestHook.java | 11 +-- .../optable/targeting/v1/core/Cache.java | 20 +---- .../targeting/v1/core/ConfigResolver.java | 34 ++++++++ .../optable/targeting/v1/core/IdsMapper.java | 19 ++--- .../targeting/v1/core/OptableTargeting.java | 59 +++++++++----- .../targeting/v1/core/QueryBuilder.java | 12 +-- .../v1/core/merger/PayloadCleaner.java | 3 + .../optable/targeting/v1/net/APIClient.java | 8 +- .../optable/targeting/v1/BaseOptableTest.java | 18 ++++- ...tableTargetingAuctionResponseHookTest.java | 16 +++- .../v1/OptableTargetingModuleTest.java | 8 +- ...getingProcessedAuctionRequestHookTest.java | 26 +++--- .../optable/targeting/v1/core/CacheTest.java | 31 ++------ .../targeting/v1/core/IdMapperTest.java | 8 +- .../v1/core/OptableTargetingTest.java | 79 +++++++++++-------- .../targeting/v1/core/QueryBuilderTest.java | 13 +-- .../targeting/v1/net/APIClientTest.java | 23 +++--- .../configs/prebid-config-with-optable.yaml | 38 --------- .../configs/sample-app-settings-optable.yaml | 44 +++++++++++ 23 files changed, 332 insertions(+), 240 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index ce40749002c..0a9094a42f0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -3,7 +3,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; import org.prebid.server.cache.PbcStorageService; -import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; @@ -11,6 +10,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -50,8 +50,8 @@ AnalyticTagsResolver analyticTagsResolver(ObjectMapper objectMapper) { } @Bean - IdsMapper queryParametersExtractor(OptableTargetingProperties properties, ObjectMapper objectMapper) { - return new IdsMapper(objectMapper, properties.getPpidMapping()); + IdsMapper queryParametersExtractor(ObjectMapper objectMapper) { + return new IdsMapper(objectMapper); } @Bean @@ -60,8 +60,8 @@ PayloadResolver payloadResolver(ObjectMapper mapper) { } @Bean - QueryBuilder queryBuilder(OptableTargetingProperties properties) { - return new QueryBuilder(properties.getIdPrefixOrder()); + QueryBuilder queryBuilder() { + return new QueryBuilder(); } @Bean @@ -86,8 +86,7 @@ APIClient apiClient(OptableHttpClientWrapper httpClientWrapper, final String endpoint = HttpUtil.validateUrl(Objects.requireNonNull(properties.getApiEndpoint())); - return new APIClient(endpoint, httpClientWrapper.getHttpClient(), logSamplingRate, responseParser, - properties.getApiKey()); + return new APIClient(endpoint, httpClientWrapper.getHttpClient(), logSamplingRate, responseParser); } @Bean @@ -96,46 +95,39 @@ OptableAttributesResolver optableAttributesResolver() { } @Bean - Cache cache(PbcStorageService cacheService, - OptableResponseMapper responseParser, - OptableTargetingProperties properties, - @Value("${storage.pbc.enabled:false}") - boolean storageEnabled, - @Value("${cache.module.enabled:false}") - boolean moduleCacheEnabled - ) { - - final CacheProperties cacheProperties = properties.getCache(); - final boolean isCacheEnabled = cacheProperties.isEnabled(); - final boolean isCacheStorageEnabled = storageEnabled && moduleCacheEnabled; - - return new Cache( - cacheService, - responseParser, - isCacheEnabled && isCacheStorageEnabled, - cacheProperties.getTtlseconds()); + Cache cache(PbcStorageService cacheService, OptableResponseMapper responseMapper) { + return new Cache(cacheService, responseMapper); } @Bean OptableTargeting optableTargeting(IdsMapper parametersExtractor, QueryBuilder queryBuilder, APIClient apiClient, - Cache cache) { + Cache cache, + @Value("${storage.pbc.enabled:false}") + boolean storageEnabled, + @Value("${cache.module.enabled:false}") + boolean moduleCacheEnabled) { + + return new OptableTargeting(cache, parametersExtractor, queryBuilder, apiClient, + storageEnabled && moduleCacheEnabled); + } - return new OptableTargeting(cache, parametersExtractor, queryBuilder, apiClient); + @Bean + ConfigResolver configResolver(ObjectMapper mapper, OptableTargetingProperties globalProperties) { + return new ConfigResolver(mapper, globalProperties); } @Bean - OptableTargetingModule optableTargetingModule(OptableTargetingProperties properties, + OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, AnalyticTagsResolver analyticTagsResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver) { return new OptableTargetingModule(List.of( - new OptableTargetingProcessedAuctionRequestHook(properties, optableTargeting, payloadResolver, + new OptableTargetingProcessedAuctionRequestHook(configResolver, optableTargeting, payloadResolver, optableAttributesResolver), - new OptableTargetingAuctionResponseHook(analyticTagsResolver, payloadResolver, - properties.getAdserverTargeting()))); + new OptableTargetingAuctionResponseHook(analyticTagsResolver, payloadResolver, configResolver))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java index cca7dada5b1..4bc3a21a48f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java @@ -1,11 +1,21 @@ package org.prebid.server.hooks.modules.optable.targeting.model.config; -import lombok.Data; +import lombok.Getter; +import lombok.Setter; -@Data +@Getter +@Setter public class CacheProperties { boolean enabled = false; int ttlseconds = 86400; + + public CacheProperties() { + } + + public CacheProperties(boolean enabled, int ttlseconds) { + this.enabled = enabled; + this.ttlseconds = ttlseconds; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 9df32381f7b..b0ca10da386 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -11,17 +11,38 @@ @Data public final class OptableTargetingProperties { + @JsonProperty("api-endpoint") String apiEndpoint; + @JsonProperty("api-key") String apiKey; + @JsonProperty("ppid-mapping") Map ppidMapping; + @JsonProperty("adserver-targeting") Boolean adserverTargeting = true; Long timeout; + @JsonProperty("id-prefix-order") String idPrefixOrder; - CacheProperties cache = new CacheProperties(); + public OptableTargetingProperties() { + } + + public OptableTargetingProperties(String apiEndpoint, String apiKey, Map ppidMapping, + Boolean adserverTargeting, Long timeout, String idPrefixOrder, + CacheProperties cache) { + + this.apiEndpoint = apiEndpoint; + this.apiKey = apiKey; + this.ppidMapping = ppidMapping; + this.adserverTargeting = adserverTargeting; + this.timeout = timeout; + this.idPrefixOrder = idPrefixOrder; + this.cache = cache; + } + + CacheProperties cache = new CacheProperties(false, 86400); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index eb57fee4970..5f4c93b450d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -7,9 +7,11 @@ import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -30,22 +32,25 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook private final PayloadResolver payloadResolver; - private final boolean adserverTargeting; + private final ConfigResolver configResolver; public OptableTargetingAuctionResponseHook( AnalyticTagsResolver analyticTagsResolver, PayloadResolver payloadResolver, - boolean adserverTargeting) { + ConfigResolver configResolver) { this.analyticTagsResolver = Objects.requireNonNull(analyticTagsResolver); this.payloadResolver = Objects.requireNonNull(payloadResolver); - this.adserverTargeting = adserverTargeting; + this.configResolver = configResolver; } @Override public Future> call(AuctionResponsePayload auctionResponsePayload, AuctionInvocationContext invocationContext) { + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + final boolean adserverTargeting = properties.getAdserverTargeting(); + final ModuleContext moduleContext = ModuleContext.of(invocationContext); moduleContext.setAdserverTargetingEnabled(adserverTargeting); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 4556ed9965f..2043eadf721 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -2,7 +2,6 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; -import org.prebid.server.cache.PbcStorageService; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -12,7 +11,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -33,7 +32,7 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private static final long DEFAULT_API_CALL_TIMEOUT = 1000L; - private final OptableTargetingProperties properties; + private final ConfigResolver configResolver; private final OptableTargeting optableTargeting; @@ -41,12 +40,12 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final OptableAttributesResolver optableAttributesResolver; - public OptableTargetingProcessedAuctionRequestHook(OptableTargetingProperties properties, + public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver) { - this.properties = Objects.requireNonNull(properties); + this.configResolver = Objects.requireNonNull(configResolver); this.optableTargeting = Objects.requireNonNull(optableTargeting); this.payloadResolver = Objects.requireNonNull(payloadResolver); this.optableAttributesResolver = Objects.requireNonNull(optableAttributesResolver); @@ -56,6 +55,7 @@ public OptableTargetingProcessedAuctionRequestHook(OptableTargetingProperties pr public Future> call(AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); final ModuleContext moduleContext = new ModuleContext() .setMetrics(Metrics.builder() .moduleStartTime(System.currentTimeMillis()) @@ -72,6 +72,7 @@ public Future> call(AuctionRequestPayloa properties.getTimeout()); final Future targetingResultFuture = optableTargeting.getTargeting( + properties, bidRequest, attributes, timeout); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index 6b8edd26b60..b90f99ce56b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -8,7 +8,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; import java.util.Objects; -import java.util.function.Function; public class Cache { @@ -20,19 +19,11 @@ public class Cache { private final OptableResponseMapper optableResponseMapper; - private final boolean isEnabled; - - private final int ttlSeconds; - public Cache(PbcStorageService cacheService, - OptableResponseMapper optableResponseMapper, - boolean isEnabled, - int ttlSeconds) { + OptableResponseMapper optableResponseMapper) { this.cacheService = Objects.requireNonNull(cacheService); this.optableResponseMapper = Objects.requireNonNull(optableResponseMapper); - this.isEnabled = isEnabled; - this.ttlSeconds = ttlSeconds; } public Future get(String query) { @@ -42,17 +33,12 @@ public Future get(String query) { .otherwise(it -> null); } - public boolean put(String query, TargetingResult value) { - cacheService.storeEntry( + public Future put(String query, TargetingResult value, int ttlSeconds) { + return cacheService.storeEntry( query, optableResponseMapper.toJsonString(value), StorageDataType.TEXT, ttlSeconds, APPLICATION, APP_CODE); - return true; - } - - public boolean isEnabled() { - return isEnabled; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java new file mode 100644 index 00000000000..f4750901e1e --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java @@ -0,0 +1,34 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; + +import java.util.Optional; + +public class ConfigResolver { + + private final ObjectMapper mapper; + + private final OptableTargetingProperties globalProperties; + + public ConfigResolver(ObjectMapper mapper, OptableTargetingProperties globalProperties) { + this.mapper = mapper; + this.globalProperties = globalProperties; + } + + public OptableTargetingProperties resolve(ObjectNode configNode) { + final ObjectNode mergedNode = ((ObjectNode) mapper.valueToTree(globalProperties)).setAll(configNode); + return parse(mergedNode).orElse(globalProperties); + } + + private Optional parse(ObjectNode configNode) { + try { + return Optional.ofNullable(configNode) + .filter(node -> !node.isEmpty()) + .map(node -> mapper.convertValue(node, OptableTargetingProperties.class)); + } catch (IllegalArgumentException e) { + return Optional.empty(); + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index 19f620ea9c6..5c226444270 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -19,18 +19,15 @@ public class IdsMapper { private final ObjectMapper objectMapper; - private final Map ppidMapping; - - public IdsMapper(ObjectMapper objectMapper, Map ppidMapping) { + public IdsMapper(ObjectMapper objectMapper) { this.objectMapper = Objects.requireNonNull(objectMapper); - this.ppidMapping = Objects.requireNonNull(ppidMapping); } - public List toIds(BidRequest bidRequest) { + public List toIds(BidRequest bidRequest, Map ppidMapping) { final IdsResolver idsResolver = IdsResolver.of(objectMapper, bidRequest); final Map ids = applyStaticMapping(idsResolver); - final Map dynamicIds = applyDynamicMapping(idsResolver); + final Map dynamicIds = applyDynamicMapping(idsResolver, ppidMapping); if (dynamicIds != null && !dynamicIds.isEmpty()) { ids.putAll(dynamicIds); } @@ -41,18 +38,18 @@ public List toIds(BidRequest bidRequest) { .toList(); } - private Map toIds(List eids) { + private Map toIds(List eids, Map ppidMapping) { if (ppidMapping == null || ppidMapping.isEmpty() || CollectionUtils.isEmpty(eids)) { return null; } return eids.stream() - .map(this::eidSourceToFirstUidId) + .map(eid -> eidSourceToFirstUidId(eid, ppidMapping)) .filter(Objects::nonNull) .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); } - private Pair eidSourceToFirstUidId(Eid eid) { + private Pair eidSourceToFirstUidId(Eid eid, Map ppidMapping) { final String key = ppidMapping.get(eid.getSource()); return key != null ? Pair.of(key, getFirstUidId(eid)) : null; @@ -62,8 +59,8 @@ private String getFirstUidId(Eid eid) { return eid.getUids().stream().findFirst().map(Uid::getId).orElse(null); } - private Map applyDynamicMapping(IdsResolver idsResolver) { - return toIds(idsResolver.getEIDs()); + private Map applyDynamicMapping(IdsResolver idsResolver, Map ppidMapping) { + return toIds(idsResolver.getEIDs(), ppidMapping); } private Map applyStaticMapping(IdsResolver idsResolver) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index f1c94dc1cd7..48311194cf5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -1,10 +1,10 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.iab.openrtb.request.BidRequest; -import io.vertx.core.AsyncResult; import io.vertx.core.Future; -import io.vertx.core.Handler; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; @@ -17,40 +17,59 @@ public class OptableTargeting { private final Cache cache; + private boolean moduleCacheEnabled = false; + + public OptableTargeting(Cache cache, IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient, + boolean moduleCacheEnabled) { - public OptableTargeting(Cache cache, IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient) { this.cache = cache; this.idsMapper = Objects.requireNonNull(idsMapper); this.queryBuilder = Objects.requireNonNull(queryBuilder); this.apiClient = Objects.requireNonNull(apiClient); + this.moduleCacheEnabled = moduleCacheEnabled; } private final IdsMapper idsMapper; private final QueryBuilder queryBuilder; private final APIClient apiClient; - public Future getTargeting(BidRequest bidRequest, OptableAttributes attributes, long timeout) { + public Future getTargeting(OptableTargetingProperties properties, BidRequest bidRequest, + OptableAttributes attributes, long timeout) { + return Optional.ofNullable(bidRequest) - .map(idsMapper::toIds) - .map(ids -> queryBuilder.build(ids, attributes)) - .map(query -> cache.isEnabled() - ? getTargetingWithCache(query, attributes.getIps(), timeout) - : apiClient.getTargeting(query, attributes.getIps(), timeout) ) + .map(it -> idsMapper.toIds(it, properties.getPpidMapping())) + .map(ids -> queryBuilder.build(ids, attributes, properties.getIdPrefixOrder())) + .map(query -> properties.getCache().isEnabled() && moduleCacheEnabled + ? getOrFetchTargetingResults( + properties.getCache(), + properties.getApiKey(), + query, + attributes.getIps(), timeout) + : apiClient.getTargeting( + properties.getApiKey(), + query, + attributes.getIps(), + timeout)) .orElse(Future.failedFuture("Can't get targeting")); } - private Future getTargetingWithCache(String query, List ips, long timeout) { - final String key = URLEncoder.encode(query, StandardCharsets.UTF_8); - return cache.get(key) - .compose(it -> it == null - ? callAPIAndCacheResult(query, ips, timeout) - : Future.succeededFuture(it)) - .recover(throwable -> callAPIAndCacheResult(query, ips, timeout)); + private Future getOrFetchTargetingResults(CacheProperties cacheProperties, String apiKey, + String query, List ips, long timeout) { + + final String key = URLEncoder.encode(query, StandardCharsets.UTF_8); + + return cache.get(key) + .recover(err -> Future.succeededFuture(null)) + .compose(entry -> entry != null + ? Future.succeededFuture(entry) + : fetchAndCacheResult(cacheProperties.getTtlseconds(), apiKey, query, ips, timeout)); + } - private Future callAPIAndCacheResult(String query, List ips, long timeout) { - return apiClient.getTargeting(query, ips, timeout) - .onComplete(targetingResultAsyncResult -> - cache.put(query, targetingResultAsyncResult.result())); + private Future fetchAndCacheResult(int ttlSeconds, String apiKey, String query, + List ips, long timeout) { + + return apiClient.getTargeting(apiKey, query, ips, timeout) + .compose(result -> cache.put(query, result, ttlSeconds).map(result)); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 44e63999ef0..ff6634b97d2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -15,19 +15,13 @@ public class QueryBuilder { - private String idPrefixOrder; - - public QueryBuilder(String idPrefixOrder) { - this.idPrefixOrder = idPrefixOrder; - } - - public String build(List ids, OptableAttributes optableAttributes) { + public String build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { if (CollectionUtils.isEmpty(ids)) { return null; } final StringBuilder sb = new StringBuilder(); - final List reorderedIds = reorderIds(ids); + final List reorderedIds = reorderIds(ids, idPrefixOrder); if (CollectionUtils.isNotEmpty(reorderedIds)) { buildQueryString(sb, reorderedIds); } @@ -36,7 +30,7 @@ public String build(List ids, OptableAttributes optableAttributes) { return sb.toString(); } - private List reorderIds(List ids) { + private List reorderIds(List ids, String idPrefixOrder) { if (!StringUtils.isEmpty(idPrefixOrder)) { final int lastIndex = ids.size() - 1; final List order = Stream.of(idPrefixOrder.split(",", -1)).toList(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java index f4cbe3e0192..fe74f1a05c4 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java @@ -8,6 +8,9 @@ public class PayloadCleaner { private static final List FIELDS_FILTER = List.of("email", "phone", "zip", "vid"); + private PayloadCleaner() { + } + public static ObjectNode cleanUserExtOptable(ObjectNode optable) { return optable.deepCopy().remove(FIELDS_FILTER); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index 294de7fcc40..abfe734062f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -39,22 +39,18 @@ public class APIClient { private final OptableResponseMapper responseMapper; - private final String apiKey; - public APIClient(String endpoint, HttpClient httpClient, double logSamplingRate, - OptableResponseMapper responseMapper, - String apiKey) { + OptableResponseMapper responseMapper) { this.endpoint = Objects.requireNonNull(endpoint); this.httpClient = Objects.requireNonNull(httpClient); this.logSamplingRate = logSamplingRate; this.responseMapper = Objects.requireNonNull(responseMapper); - this.apiKey = apiKey; } - public Future getTargeting(String query, List ips, long timeout) { + public Future getTargeting(String apiKey, String query, List ips, long timeout) { final MultiMap headers = HeadersMultiMap.headers() .add(HttpUtil.ACCEPT_HEADER, "application/json"); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 1ef1639e63d..cfee34500b9 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -22,12 +22,15 @@ import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; @@ -38,10 +41,11 @@ import java.nio.file.Paths; import java.util.Collections; import java.util.List; +import java.util.Map; public abstract class BaseOptableTest { - private final ObjectMapper mapper = new ObjectMapper(); + protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); protected ModuleContext givenModuleContext() { return givenModuleContext(null); @@ -167,4 +171,16 @@ protected String givenBodyFromFile(String fileName) { } } } + + protected OptableTargetingProperties givenOptableTargetingProperties(boolean enableCache) { + return new OptableTargetingProperties( + "endpoint", + "key", + Map.of("c", "id"), + true, + 100L, + null, + new CacheProperties(enableCache, 86400) + ); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 9e2df353898..3908e5113f2 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -14,6 +14,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -25,6 +26,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mock.Strictness.LENIENT; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -32,7 +34,7 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { @Mock AuctionResponsePayload auctionResponsePayload; - @Mock + @Mock(strictness = LENIENT) AuctionInvocationContext invocationContext; private PayloadResolver payloadResolver; @@ -43,13 +45,17 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { private AuctionResponseHook target; + private ConfigResolver configResolver; + @BeforeEach public void setUp() { + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); payloadResolver = new PayloadResolver(mapper); + configResolver = new ConfigResolver(mapper, givenOptableTargetingProperties(false)); target = new OptableTargetingAuctionResponseHook( new AnalyticTagsResolver(mapper), payloadResolver, - true); + configResolver); } @Test @@ -126,7 +132,7 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { target = new OptableTargetingAuctionResponseHook( new AnalyticTagsResolver(mapper), payloadResolver, - false); + configResolver); // when final Future> future = target.call(auctionResponsePayload, @@ -140,4 +146,8 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.no_action, InvocationResult::action); } + + private ObjectNode givenAccountConfig(boolean cacheEnabled) { + return mapper.valueToTree(givenOptableTargetingProperties(cacheEnabled)); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 5f2beebd00e..052ff8490d2 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -4,8 +4,8 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -22,7 +22,7 @@ public class OptableTargetingModuleTest { @Mock - OptableTargetingProperties properties; + ConfigResolver configResolver; @Mock OptableTargeting optableTargeting; @@ -53,14 +53,14 @@ public void shouldReturnHooks() { // given final Collection> hooks = List.of(new OptableTargetingProcessedAuctionRequestHook( - properties, + configResolver, optableTargeting, payloadResolver, optableAttributesResolver), new OptableTargetingAuctionResponseHook( analyticTagsResolver, payloadResolver, - true)); + configResolver)); final Module module = new OptableTargetingModule(hooks); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index cb0d85421a3..5258d14cdec 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; @@ -13,7 +12,7 @@ import org.mockito.quality.Strictness; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; -import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -22,7 +21,6 @@ import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import org.prebid.server.json.ObjectMapperProvider; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -33,8 +31,6 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { - @Mock - OptableTargetingProperties optableTargetingProperties; @Mock OptableTargeting optableTargeting; @Mock @@ -44,7 +40,7 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock Timeout timeout; - private ObjectMapper mapper; + private ConfigResolver configResolver; private PayloadResolver payloadResolver; @@ -55,11 +51,12 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @BeforeEach public void setUp() { when(timeout.remaining()).thenReturn(1000L); + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(timeout)); - mapper = ObjectMapperProvider.mapper(); + configResolver = new ConfigResolver(mapper, givenOptableTargetingProperties(false)); payloadResolver = new PayloadResolver(mapper); optableAttributesResolver = new OptableAttributesResolver(); - target = new OptableTargetingProcessedAuctionRequestHook(optableTargetingProperties, optableTargeting, + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, optableTargeting, payloadResolver, optableAttributesResolver); } @@ -73,7 +70,7 @@ public void shouldHaveRightCode() { public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), anyLong())) + when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -101,8 +98,9 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin @Test public void shouldReturnResultWithCleanedUpUserExtOptableTag() { // given + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(false)); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), anyLong())) + when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -131,7 +129,7 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() { public void shouldReturnResultWithoutUpdateActionWhenBidRequestIsNull() { // given when(auctionRequestPayload.bidRequest()).thenReturn(null); - when(optableTargeting.getTargeting(any(), any(), anyLong())) + when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -153,7 +151,7 @@ public void shouldReturnResultWithoutUpdateActionWhenBidRequestIsNull() { public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), anyLong())).thenReturn(Future.succeededFuture(null)); + when(optableTargeting.getTargeting(any(), any(), any(), anyLong())).thenReturn(Future.succeededFuture(null)); // when final Future> future = target.call(auctionRequestPayload, @@ -169,4 +167,8 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() assertThat(result.action()).isEqualTo(InvocationAction.update); assertThat(result.errors()).isNull(); } + + private ObjectNode givenAccountConfig(boolean cacheEnabled) { + return mapper.valueToTree(givenOptableTargetingProperties(cacheEnabled)); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java index 2fa1b2a193e..7dbfda47a71 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -42,28 +42,7 @@ public class CacheTest { @BeforeEach public void setUp() { - target = new Cache(pbcStorageService, optableResponseMapper, true, 86400); - } - - @Test - public void cacheShouldBeEnabled() { - // given and when - boolean isEnabled = target.isEnabled(); - - // then - Assertions.assertThat(isEnabled).isTrue(); - } - - @Test - public void cacheShouldBeDisabled() { - // given - target = new Cache(pbcStorageService, optableResponseMapper, false, 86400); - - // when - boolean isEnabled = target.isEnabled(); - - // then - Assertions.assertThat(isEnabled).isFalse(); + target = new Cache(pbcStorageService, optableResponseMapper); } @Test @@ -73,7 +52,7 @@ public void cacheShouldNotCallMapperIfNoEntry() { Future.succeededFuture(ModuleCacheResponse.empty())); // when - TargetingResult result = target.get("key").result(); + final TargetingResult result = target.get("key").result(); // then Assertions.assertThat(result).isNull(); @@ -89,7 +68,7 @@ public void cacheShouldReturnEntry() { mapper.encodeToString(targetingResult)))); // when - TargetingResult result = target.get("key").result(); + final TargetingResult result = target.get("key").result(); // then Assertions.assertThat(result) @@ -105,7 +84,9 @@ public void cacheShouldStoreEntry() { final TargetingResult targetingResult = givenTargetingResult(); // when - final boolean result = target.put("key", targetingResult); + when(pbcStorageService.storeEntry(any(), any(), any(), any(), any(), any())) + .thenReturn(Future.succeededFuture()); + final boolean result = target.put("key", targetingResult, 86400).succeeded(); // then Assertions.assertThat(result).isTrue(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java index c83fd1f2983..5f63f91afb8 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java @@ -26,9 +26,11 @@ public class IdMapperTest { private final ObjectMapper objectMapper = new ObjectMapper(); private IdsMapper target; + private Map ppidMapping = Map.of("test.com", "c"); + @BeforeEach public void setUp() { - target = new IdsMapper(objectMapper, Map.of("test.com", "c")); + target = new IdsMapper(objectMapper); } @Test @@ -55,7 +57,7 @@ public void shouldMapBidRequestToAllPossibleIds() { }); // when - final List ids = target.toIds(bidRequest); + final List ids = target.toIds(bidRequest, ppidMapping); // then assertThat(ids).isNotNull() @@ -90,7 +92,7 @@ public void shouldMapNothing() { final BidRequest bidRequest = givenBidRequest(bidRequestBuilder -> bidRequestBuilder); // when - final List ids = target.toIds(bidRequest); + final List ids = target.toIds(bidRequest, ppidMapping); // then assertThat(ids).isNotNull(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 204344eba14..3f96e78cc02 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -9,6 +9,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; @@ -19,6 +20,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; @@ -38,29 +40,35 @@ public class OptableTargetingTest extends BaseOptableTest { @Mock private APIClient apiClient; - private QueryBuilder queryBuilder = new QueryBuilder("c,c1,email"); + private QueryBuilder queryBuilder = new QueryBuilder(); private OptableTargeting target; private OptableAttributes optableAttributes; + private String idPrefixOrder = "c,c1,email"; + + private OptableTargetingProperties properties; + @BeforeEach public void setUp() { - when(cache.isEnabled()).thenReturn(false); optableAttributes = givenOptableAttributes(); - target = new OptableTargeting(cache, idsMapper, queryBuilder, apiClient); + properties = givenOptableTargetingProperties(true); + target = new OptableTargeting(cache, idsMapper, queryBuilder, apiClient, true); } @Test public void shouldReturnTargetingResultsAndNotUseCache() { // given final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), anyLong())) + properties = givenOptableTargetingProperties(false); + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting( + properties, bidRequest, optableAttributes, 100); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -70,22 +78,23 @@ public void shouldReturnTargetingResultsAndNotUseCache() { .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); verify(cache, times(0)).get(any()); - verify(apiClient, times(1)).getTargeting(any(), any(), anyLong()); - verify(cache, times(0)).put(any(), eq(targetingResult.result())); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), anyLong()); + verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @Test public void shouldCallAPIAndAddTargetingResultsToCache() { // given - when(cache.isEnabled()).thenReturn(true); when(cache.get(any())).thenReturn(Future.succeededFuture(null)); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), anyLong())) + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting( + properties, bidRequest, optableAttributes, 100); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -94,21 +103,22 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(cache).put(any(), eq(targetingResult.result())); + verify(cache).put(any(), eq(targetingResult.result()), anyInt()); } @Test public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() { // given - when(cache.isEnabled()).thenReturn(true); when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), anyLong())) + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting( + properties, bidRequest, optableAttributes, 100); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -117,20 +127,20 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(apiClient, times(1)).getTargeting(any(), any(), anyLong()); - verify(cache).put(any(), eq(targetingResult.result())); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), anyLong()); + verify(cache).put(any(), eq(targetingResult.result()), anyInt()); } @Test public void shouldUseCachedResult() { // given - when(cache.isEnabled()).thenReturn(true); when(cache.get(any())).thenReturn(Future.succeededFuture(givenTargetingResult())); final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting( + properties, bidRequest, optableAttributes, 100); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -140,19 +150,20 @@ public void shouldUseCachedResult() { .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); verify(cache, times(1)).get(any()); - verify(apiClient, times(0)).getTargeting(any(), any(), anyLong()); - verify(cache, times(0)).put(any(), eq(targetingResult.result())); + verify(apiClient, times(0)).getTargeting(any(), any(), any(), anyLong()); + verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @Test public void shouldNotFailWhenNoIdsMapped() { // given final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of()); + when(idsMapper.toIds(any(), any())).thenReturn(List.of()); verifyNoInteractions(apiClient); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting( + properties, bidRequest, optableAttributes, 100); // then assertThat(targetingResult.result()).isNull(); @@ -161,12 +172,14 @@ public void shouldNotFailWhenNoIdsMapped() { @Test public void shouldNotFailWhenApiClientIsFailed() { // given + properties = givenOptableTargetingProperties(false); final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), anyLong())).thenReturn(null); + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), any(), anyLong())).thenReturn(null); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting(properties, bidRequest, + optableAttributes, 100); // then assertThat(targetingResult.result()).isNull(); @@ -176,11 +189,13 @@ public void shouldNotFailWhenApiClientIsFailed() { public void shouldNotFailWhenApiClientReturnsFailFuture() { // given final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), anyLong())).thenReturn(Future.failedFuture("File")); + properties = givenOptableTargetingProperties(false); + when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); + when(apiClient.getTargeting(any(), any(), any(), anyLong())).thenReturn(Future.failedFuture("File")); // when - final Future targetingResult = target.getTargeting(bidRequest, optableAttributes, 100); + final Future targetingResult = target.getTargeting(properties, bidRequest, + optableAttributes, 100); // then assertThat(targetingResult.result()).isNull(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 6673883c95b..faccf87d021 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -15,10 +15,13 @@ public class QueryBuilderTest { private OptableAttributes optableAttributes; + private String idPrefixOrder; + @BeforeEach public void setUp() { optableAttributes = givenOptableAttributes(); - target = new QueryBuilder("c,c1"); + target = new QueryBuilder(); + idPrefixOrder = "c,c1"; } @Test @@ -27,7 +30,7 @@ public void shouldBuildQueryStringWhenHaveIds() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = target.build(ids, optableAttributes); + final String query = target.build(ids, optableAttributes, idPrefixOrder); // then assertThat(query).contains("e%3Aemail", "p%3A123"); @@ -39,7 +42,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = target.build(ids, optableAttributes); + final String query = target.build(ids, optableAttributes, idPrefixOrder); // then assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); @@ -52,7 +55,7 @@ public void shouldBuildQueryStringWithRightOrder() { Id.of("c", "234")); // when - final String query = target.build(ids, optableAttributes); + final String query = target.build(ids, optableAttributes, idPrefixOrder); // then assertThat(query).startsWith("c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); @@ -64,7 +67,7 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmpty() { final List ids = List.of(); // when - final String query = target.build(ids, optableAttributes); + final String query = target.build(ids, optableAttributes, idPrefixOrder); // then diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index 917343b1dfe..affeafcaef1 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -51,8 +51,7 @@ public void setUp() { target = new APIClient("endpoint", httpClient, LOG_SAMPLING_RATE, - parser, - null); + parser); } @Test @@ -62,7 +61,7 @@ public void shouldReturnTargetingResult() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNotNull(); @@ -79,7 +78,7 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -92,7 +91,7 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -105,7 +104,7 @@ public void shouldNotFailWhenHttpClientIsCrashed() { .thenThrow(new NullPointerException()); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -119,7 +118,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -131,15 +130,14 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { target = new APIClient("endpoint", httpClient, LOG_SAMPLING_RATE, - parser, - "key"); + parser); when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -158,7 +156,7 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting(null, "query", List.of("8.8.8.8"), 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -178,6 +176,7 @@ public void shouldPassThroughIpAddresses() { // when final Future result = target.getTargeting( + "key", "query", List.of("8.8.8.8", "2001:4860:4860::8888"), 1000); @@ -198,7 +197,7 @@ public void shouldNotPassThroughIpAddressWhenNotSpecified() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("query", null, 1000); + final Future result = target.getTargeting("key", "query", null, 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index df3852ace2c..ef78340c3fb 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -48,44 +48,6 @@ admin-endpoints: hooks: optable-targeting: enabled: true - host-execution-plan: > - { - "endpoints": { - "/openrtb2/auction": { - "stages": { - "processed-auction-request": { - "groups": [ - { - "timeout": 600, - "hook-sequence": [ - { - "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-processed-auction-request-hook" - } - ] - } - ] - }, - "auction-response": { - "groups": [ - { - "timeout": 10, - "hook-sequence": [ - { - "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-auction-response-hook" - } - ] - } - ] - } - } - } - } - } modules: optable-targeting: api-endpoint: endpoint - api-key: key - ppid-mapping: {"pubcid.org": "c"} - adserver-targeting: true diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 2671a6a7642..393b9944c94 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -15,3 +15,47 @@ accounts: default: true analytics: allow-client-details: true + hooks: + modules: + optable-targeting: + api-key: key + ppid-mapping: { "pubcid.org": "c" } + adserver-targeting: true + cache: + enabled: false + ttlseconds: 86400 + execution-plan: + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 600, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] + }, + "auction-response": { + "groups": [ + { + "timeout": 10, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-auction-response-hook" + } + ] + } + ] + } + } + } + } + } From c91eea7a638c062cd95d0fb8b7c13fcaee68f557 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 14 Apr 2025 10:58:58 +0200 Subject: [PATCH 11/41] optable-targeting: Split API endpoint configuration into host-level and account-level settings --- .../sample-requests/data.json | 2 +- .../config/OptableTargetingProperties.java | 5 ++++ .../optable/targeting/v1/core/Cache.java | 4 ++- .../targeting/v1/core/EndpointResolver.java | 16 ++++++++++++ .../targeting/v1/core/OptableTargeting.java | 18 ++++++++----- .../optable/targeting/v1/net/APIClient.java | 9 ++++--- .../v1/core/OptableTargetingTest.java | 17 ++++++------ .../targeting/v1/net/APIClientTest.java | 26 +++++++++++++------ .../configs/prebid-config-with-optable.yaml | 2 +- .../configs/sample-app-settings-optable.yaml | 2 ++ 10 files changed, 73 insertions(+), 28 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java diff --git a/extra/modules/optable-targeting/sample-requests/data.json b/extra/modules/optable-targeting/sample-requests/data.json index 8ce26abedb6..f0ef7e2cf9c 100644 --- a/extra/modules/optable-targeting/sample-requests/data.json +++ b/extra/modules/optable-targeting/sample-requests/data.json @@ -64,7 +64,7 @@ "source": "pubcid.org", "uids": [ { - "id": "f9528392fd38786082c7fd01cd2aa5a60d2c3a2788789b1f9de4574a7c9dabae", + "id": "test", "atype": 1 } ] diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index b0ca10da386..064951b61a6 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -17,6 +17,11 @@ public final class OptableTargetingProperties { @JsonProperty("api-key") String apiKey; + @JsonProperty("user-id") + String userId; + + String origin; + @JsonProperty("ppid-mapping") Map ppidMapping; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index b90f99ce56b..d59614c45e7 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -29,11 +29,13 @@ public Cache(PbcStorageService cacheService, public Future get(String query) { return cacheService.retrieveEntry(query, APP_CODE, APPLICATION) .map(ModuleCacheResponse::getValue) - .map(optableResponseMapper::parse) + .map(it -> it != null ? optableResponseMapper.parse(it) : null) .otherwise(it -> null); } public Future put(String query, TargetingResult value, int ttlSeconds) { + if (value == null) return Future.succeededFuture(); + return cacheService.storeEntry( query, optableResponseMapper.toJsonString(value), diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java new file mode 100644 index 00000000000..f14a0aa2722 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java @@ -0,0 +1,16 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +public class EndpointResolver { + + private static final String ACCOUNT_ID = "{ACCOUNT_ID}"; + + private static final String ORIGIN = "{ORIGIN}"; + + private EndpointResolver() { + } + + public static String resolve(String endpointTemplate, String accountId, String origin) { + return endpointTemplate.replace(ACCOUNT_ID, accountId) + .replace(ORIGIN, origin); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 48311194cf5..d9e2e4678bd 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -43,10 +43,14 @@ public Future getTargeting(OptableTargetingProperties propertie ? getOrFetchTargetingResults( properties.getCache(), properties.getApiKey(), + properties.getUserId(), + properties.getOrigin(), query, attributes.getIps(), timeout) : apiClient.getTargeting( properties.getApiKey(), + properties.getUserId(), + properties.getOrigin(), query, attributes.getIps(), timeout)) @@ -54,22 +58,24 @@ public Future getTargeting(OptableTargetingProperties propertie } private Future getOrFetchTargetingResults(CacheProperties cacheProperties, String apiKey, + String accountId, String origin, String query, List ips, long timeout) { - final String key = URLEncoder.encode(query, StandardCharsets.UTF_8); + final String key = accountId + ":" +URLEncoder.encode(query, StandardCharsets.UTF_8); return cache.get(key) .recover(err -> Future.succeededFuture(null)) .compose(entry -> entry != null ? Future.succeededFuture(entry) - : fetchAndCacheResult(cacheProperties.getTtlseconds(), apiKey, query, ips, timeout)); + : fetchAndCacheResult(accountId, origin, cacheProperties.getTtlseconds(), apiKey, + query, ips, timeout)); } - private Future fetchAndCacheResult(int ttlSeconds, String apiKey, String query, - List ips, long timeout) { + private Future fetchAndCacheResult(String accountId, String origin, int ttlSeconds, String apiKey, + String query, List ips, long timeout) { - return apiClient.getTargeting(apiKey, query, ips, timeout) - .compose(result -> cache.put(query, result, ttlSeconds).map(result)); + return apiClient.getTargeting(apiKey, accountId, origin, query, ips, timeout) + .compose(result -> cache.put(accountId + ":" + query, result, ttlSeconds).map(result)); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index abfe734062f..b25a1c35a4d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -13,6 +13,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableError; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.EndpointResolver; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -50,7 +51,9 @@ public APIClient(String endpoint, this.responseMapper = Objects.requireNonNull(responseMapper); } - public Future getTargeting(String apiKey, String query, List ips, long timeout) { + public Future getTargeting(String apiKey, String accountId, String origin, + String query, List ips, long timeout) { + final MultiMap headers = HeadersMultiMap.headers() .add(HttpUtil.ACCEPT_HEADER, "application/json"); @@ -65,7 +68,7 @@ public Future getTargeting(String apiKey, String query, List logParsingError(Throwable exception, HttpRequest private Future createRequest(HttpRequest httpRequest, long remainingTimeout) { try { return httpClient.request(HttpMethod.GET, - httpRequest.getUri() + "?id=" + httpRequest.getQuery(), + httpRequest.getUri() + "&id=" + httpRequest.getQuery(), httpRequest.getHeaders(), (String) null, remainingTimeout); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 3f96e78cc02..66475a5aac1 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -63,7 +63,7 @@ public void shouldReturnTargetingResultsAndNotUseCache() { final BidRequest bidRequest = givenBidRequest(); properties = givenOptableTargetingProperties(false); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -78,7 +78,7 @@ public void shouldReturnTargetingResultsAndNotUseCache() { .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); verify(cache, times(0)).get(any()); - verify(apiClient, times(1)).getTargeting(any(), any(), any(), anyLong()); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), any(), any(), anyLong()); verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @@ -89,7 +89,7 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final BidRequest bidRequest = givenBidRequest(); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -113,7 +113,7 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final BidRequest bidRequest = givenBidRequest(); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -127,7 +127,7 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(apiClient, times(1)).getTargeting(any(), any(), any(), anyLong()); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), any(), any(), anyLong()); verify(cache).put(any(), eq(targetingResult.result()), anyInt()); } @@ -150,7 +150,7 @@ public void shouldUseCachedResult() { .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); verify(cache, times(1)).get(any()); - verify(apiClient, times(0)).getTargeting(any(), any(), any(), anyLong()); + verify(apiClient, times(0)).getTargeting(any(), any(), any(), any(), any(), anyLong()); verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @@ -175,7 +175,7 @@ public void shouldNotFailWhenApiClientIsFailed() { properties = givenOptableTargetingProperties(false); final BidRequest bidRequest = givenBidRequest(); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), anyLong())).thenReturn(null); + when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())).thenReturn(null); // when final Future targetingResult = target.getTargeting(properties, bidRequest, @@ -191,7 +191,8 @@ public void shouldNotFailWhenApiClientReturnsFailFuture() { final BidRequest bidRequest = givenBidRequest(); properties = givenOptableTargetingProperties(false); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), anyLong())).thenReturn(Future.failedFuture("File")); + when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) + .thenReturn(Future.failedFuture("File")); // when final Future targetingResult = target.getTargeting(properties, bidRequest, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index affeafcaef1..54048ecd748 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -61,7 +61,8 @@ public void shouldReturnTargetingResult() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); // when - final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNotNull(); @@ -78,7 +79,8 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); // when - final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -91,7 +93,8 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -104,7 +107,8 @@ public void shouldNotFailWhenHttpClientIsCrashed() { .thenThrow(new NullPointerException()); // when - final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -118,7 +122,8 @@ public void shouldNotFailWhenInternalErrorOccurs() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -137,7 +142,8 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -156,7 +162,8 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { "plain_text_response.json"))); // when - final Future result = target.getTargeting(null, "query", List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting(null, "accountId", "origin", + "query", List.of("8.8.8.8"), 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -177,6 +184,8 @@ public void shouldPassThroughIpAddresses() { // when final Future result = target.getTargeting( "key", + "accountId", + "origin", "query", List.of("8.8.8.8", "2001:4860:4860::8888"), 1000); @@ -197,7 +206,8 @@ public void shouldNotPassThroughIpAddressWhenNotSpecified() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "query", null, 1000); + final Future result = target.getTargeting("key", "accountId", "origin", + "query", null, 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index ef78340c3fb..df464e85627 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -50,4 +50,4 @@ hooks: enabled: true modules: optable-targeting: - api-endpoint: endpoint + api-endpoint: https://na.edge.optable.co/v2/targeting?t={ACCOUNT_ID}&o={ORIGIN} diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 393b9944c94..2fe777b320f 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -19,6 +19,8 @@ accounts: modules: optable-targeting: api-key: key + user-id: optable + origin: web-sdk-demo ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: From eff725fee6b7bcac30731c1a332d961e7f9c4d0c Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Wed, 16 Apr 2025 22:20:02 +0200 Subject: [PATCH 12/41] optable-targeting: Code cleanup --- .../model/config/OptableTargetingProperties.java | 3 +-- .../targeting/v1/core/EndpointResolver.java | 6 +++--- .../targeting/v1/core/OptableTargeting.java | 16 ++++++++-------- .../optable/targeting/v1/net/APIClient.java | 4 ++-- sample/configs/prebid-config-with-optable.yaml | 2 +- sample/configs/sample-app-settings-optable.yaml | 2 +- 6 files changed, 16 insertions(+), 17 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 064951b61a6..ed92779f4cf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -17,8 +17,7 @@ public final class OptableTargetingProperties { @JsonProperty("api-key") String apiKey; - @JsonProperty("user-id") - String userId; + String tenant; String origin; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java index f14a0aa2722..021334ce3c8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java @@ -2,15 +2,15 @@ public class EndpointResolver { - private static final String ACCOUNT_ID = "{ACCOUNT_ID}"; + private static final String TENANT = "{TENANT}"; private static final String ORIGIN = "{ORIGIN}"; private EndpointResolver() { } - public static String resolve(String endpointTemplate, String accountId, String origin) { - return endpointTemplate.replace(ACCOUNT_ID, accountId) + public static String resolve(String endpointTemplate, String tenant, String origin) { + return endpointTemplate.replace(TENANT, tenant) .replace(ORIGIN, origin); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index d9e2e4678bd..ef5b8c4228c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -43,13 +43,13 @@ public Future getTargeting(OptableTargetingProperties propertie ? getOrFetchTargetingResults( properties.getCache(), properties.getApiKey(), - properties.getUserId(), + properties.getTenant(), properties.getOrigin(), query, attributes.getIps(), timeout) : apiClient.getTargeting( properties.getApiKey(), - properties.getUserId(), + properties.getTenant(), properties.getOrigin(), query, attributes.getIps(), @@ -58,24 +58,24 @@ public Future getTargeting(OptableTargetingProperties propertie } private Future getOrFetchTargetingResults(CacheProperties cacheProperties, String apiKey, - String accountId, String origin, + String tenant, String origin, String query, List ips, long timeout) { - final String key = accountId + ":" +URLEncoder.encode(query, StandardCharsets.UTF_8); + final String key = tenant + ":" +URLEncoder.encode(query, StandardCharsets.UTF_8); return cache.get(key) .recover(err -> Future.succeededFuture(null)) .compose(entry -> entry != null ? Future.succeededFuture(entry) - : fetchAndCacheResult(accountId, origin, cacheProperties.getTtlseconds(), apiKey, + : fetchAndCacheResult(tenant, origin, cacheProperties.getTtlseconds(), apiKey, query, ips, timeout)); } - private Future fetchAndCacheResult(String accountId, String origin, int ttlSeconds, String apiKey, + private Future fetchAndCacheResult(String tenant, String origin, int ttlSeconds, String apiKey, String query, List ips, long timeout) { - return apiClient.getTargeting(apiKey, accountId, origin, query, ips, timeout) - .compose(result -> cache.put(accountId + ":" + query, result, ttlSeconds).map(result)); + return apiClient.getTargeting(apiKey, tenant, origin, query, ips, timeout) + .compose(result -> cache.put(tenant + ":" + query, result, ttlSeconds).map(result)); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index b25a1c35a4d..e23543b067b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -51,7 +51,7 @@ public APIClient(String endpoint, this.responseMapper = Objects.requireNonNull(responseMapper); } - public Future getTargeting(String apiKey, String accountId, String origin, + public Future getTargeting(String apiKey, String tenant, String origin, String query, List ips, long timeout) { final MultiMap headers = HeadersMultiMap.headers() @@ -68,7 +68,7 @@ public Future getTargeting(String apiKey, String accountId, Str } final HttpRequest request = HttpRequest.builder() - .uri(EndpointResolver.resolve(endpoint, accountId, origin)) + .uri(EndpointResolver.resolve(endpoint, tenant, origin)) .query(query) .headers(headers) .build(); diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index df464e85627..cd2d3b7d4ec 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -50,4 +50,4 @@ hooks: enabled: true modules: optable-targeting: - api-endpoint: https://na.edge.optable.co/v2/targeting?t={ACCOUNT_ID}&o={ORIGIN} + api-endpoint: https://na.edge.optable.co/v2/targeting?t={TENANT}&o={ORIGIN} diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 2fe777b320f..7a533da3697 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -19,7 +19,7 @@ accounts: modules: optable-targeting: api-key: key - user-id: optable + tenant: optable origin: web-sdk-demo ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true From 7df560bd43f7a50cda17faa5fd88881b3681fb48 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 17 Apr 2025 11:18:03 +0200 Subject: [PATCH 13/41] optable-targeting: Update caching key generation --- .../optable/targeting/model/CachingKey.java | 36 ++++++++++++++ .../optable/targeting/model/Query.java | 15 ++++++ .../optable/targeting/v1/core/Cache.java | 4 +- .../targeting/v1/core/OptableTargeting.java | 19 ++++---- .../targeting/v1/core/QueryBuilder.java | 13 +++-- .../optable/targeting/v1/net/APIClient.java | 5 +- .../targeting/model/CachingKeyTest.java | 48 +++++++++++++++++++ .../optable/targeting/v1/BaseOptableTest.java | 5 ++ .../targeting/v1/core/QueryBuilderTest.java | 35 ++++++++++++-- .../targeting/v1/net/APIClientTest.java | 18 +++---- 10 files changed, 170 insertions(+), 28 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java new file mode 100644 index 00000000000..114916663b7 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java @@ -0,0 +1,36 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.AllArgsConstructor; +import org.apache.commons.collections4.CollectionUtils; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +@AllArgsConstructor(staticName = "of") +public class CachingKey { + + String tenant; + + String origin; + + Query query; + + List ips; + + public String toString() { + return "%s:%s:%s:%s".formatted( + tenant, + origin, + CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", + query.getIds()); + } + + public String toEncodedString() { + return "%s:%s:%s:%s".formatted( + tenant, + origin, + CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", + URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8)); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java new file mode 100644 index 00000000000..20862050f39 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -0,0 +1,15 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class Query { + + String ids; + + String attributes; + + public String toQueryString() { + return ids + attributes; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index d59614c45e7..5c3b14ef240 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -34,7 +34,9 @@ public Future get(String query) { } public Future put(String query, TargetingResult value, int ttlSeconds) { - if (value == null) return Future.succeededFuture(); + if (value == null) { + return Future.succeededFuture(); + } return cacheService.storeEntry( query, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index ef5b8c4228c..8866751f665 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -2,14 +2,14 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import org.prebid.server.hooks.modules.optable.targeting.model.CachingKey; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -59,23 +59,24 @@ public Future getTargeting(OptableTargetingProperties propertie private Future getOrFetchTargetingResults(CacheProperties cacheProperties, String apiKey, String tenant, String origin, - String query, List ips, long timeout) { + Query query, List ips, long timeout) { - final String key = tenant + ":" +URLEncoder.encode(query, StandardCharsets.UTF_8); + final CachingKey cachingKey = CachingKey.of(tenant, origin, query, ips); - return cache.get(key) + return cache.get(cachingKey.toEncodedString()) .recover(err -> Future.succeededFuture(null)) .compose(entry -> entry != null ? Future.succeededFuture(entry) - : fetchAndCacheResult(tenant, origin, cacheProperties.getTtlseconds(), apiKey, + : fetchAndCacheResult(cachingKey, tenant, origin, cacheProperties.getTtlseconds(), apiKey, query, ips, timeout)); } - private Future fetchAndCacheResult(String tenant, String origin, int ttlSeconds, String apiKey, - String query, List ips, long timeout) { + private Future fetchAndCacheResult(CachingKey cachingKey, String tenant, String origin, + int ttlSeconds, String apiKey, Query query, List ips, + long timeout) { return apiClient.getTargeting(apiKey, tenant, origin, query, ips, timeout) - .compose(result -> cache.put(tenant + ":" + query, result, ttlSeconds).map(result)); + .compose(result -> cache.put(cachingKey.toString(), result, ttlSeconds).map(result)); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index ff6634b97d2..fd3a411fde6 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -4,6 +4,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -15,17 +16,20 @@ public class QueryBuilder { - public String build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { + public Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { if (CollectionUtils.isEmpty(ids)) { return null; } + return Query.of(buildIdsString(ids, idPrefixOrder), buildAttributesString(optableAttributes)); + } + + private String buildIdsString(List ids, String idPrefixOrder) { final StringBuilder sb = new StringBuilder(); final List reorderedIds = reorderIds(ids, idPrefixOrder); if (CollectionUtils.isNotEmpty(reorderedIds)) { buildQueryString(sb, reorderedIds); } - addAttributes(sb, optableAttributes); return sb.toString(); } @@ -51,7 +55,8 @@ private int checkOrder(Id item, List order, int lastIndex) { return value; } - private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes) { + private String buildAttributesString(OptableAttributes optableAttributes) { + final StringBuilder sb = new StringBuilder(); Optional.ofNullable(optableAttributes.getGdprConsent()).ifPresent(consent -> sb.append("&gdpr_consent=").append(consent)); Optional.of(optableAttributes.isGdprApplies()).ifPresent(applies -> @@ -65,6 +70,8 @@ private void addAttributes(StringBuilder sb, OptableAttributes optableAttributes }); Optional.ofNullable(optableAttributes.getTimeout()).ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms")); + + return sb.toString(); } private void buildQueryString(StringBuilder sb, List ids) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index e23543b067b..c6c826813ea 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -8,6 +8,7 @@ import io.vertx.core.http.impl.headers.HeadersMultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; @@ -52,7 +53,7 @@ public APIClient(String endpoint, } public Future getTargeting(String apiKey, String tenant, String origin, - String query, List ips, long timeout) { + Query query, List ips, long timeout) { final MultiMap headers = HeadersMultiMap.headers() .add(HttpUtil.ACCEPT_HEADER, "application/json"); @@ -69,7 +70,7 @@ public Future getTargeting(String apiKey, String tenant, String final HttpRequest request = HttpRequest.builder() .uri(EndpointResolver.resolve(endpoint, tenant, origin)) - .query(query) + .query(query.toQueryString()) .headers(headers) .build(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java new file mode 100644 index 00000000000..2d167610c67 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java @@ -0,0 +1,48 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +public class CachingKeyTest { + + @Test + public void shouldBuildCachingUrlEncodedKey() { + // given + final Query query = Query.of("query?String", "&attributes"); + final CachingKey cachingKey = CachingKey.of("tenant", "origin", query, List.of("8.8.8.8")); + + // when + final String key = cachingKey.toEncodedString(); + + // then + Assertions.assertThat(key).isEqualTo("tenant:origin:8.8.8.8:query%3FString"); + } + + @Test + public void shouldBuildCachingKey() { + // given + final Query query = Query.of("query?String", "&attributes"); + final CachingKey cachingKey = CachingKey.of("tenant", "origin", query, List.of("8.8.8.8")); + + // when + final String key = cachingKey.toString(); + + // then + Assertions.assertThat(key).isEqualTo("tenant:origin:8.8.8.8:query?String"); + } + + @Test + public void shouldPutNoneIfNoIpAddress() { + // given + final Query query = Query.of("query?String", "&attributes"); + final CachingKey cachingKey = CachingKey.of("tenant", "origin", query, null); + + // when + final String key = cachingKey.toString(); + + // then + Assertions.assertThat(key).isEqualTo("tenant:origin:none:query?String"); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index cfee34500b9..0349cd9534d 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -22,6 +22,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; @@ -183,4 +184,8 @@ protected OptableTargetingProperties givenOptableTargetingProperties(boolean ena new CacheProperties(enableCache, 86400) ); } + + protected Query givenQuery() { + return Query.of("que", "ry"); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index faccf87d021..618c9bf1276 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; import java.util.List; @@ -24,13 +25,39 @@ public void setUp() { idPrefixOrder = "c,c1"; } + @Test + public void shouldSeparateAttributesFromIds() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final Query query = target.build(ids, optableAttributes, idPrefixOrder); + + // then + assertThat(query.getIds()).isEqualTo("e%3Aemail&id=p%3A123"); + assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms"); + } + + @Test + public void shouldBuildFullQueryString() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); + + // when + final Query query = target.build(ids, optableAttributes, idPrefixOrder); + + // then + assertThat(query.getIds()).isEqualTo("e%3Aemail&id=p%3A123"); + assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms"); + } + @Test public void shouldBuildQueryStringWhenHaveIds() { // given final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder); + final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then assertThat(query).contains("e%3Aemail", "p%3A123"); @@ -42,7 +69,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder); + final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); @@ -55,7 +82,7 @@ public void shouldBuildQueryStringWithRightOrder() { Id.of("c", "234")); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder); + final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then assertThat(query).startsWith("c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); @@ -67,7 +94,7 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmpty() { final List ids = List.of(); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder); + final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index 54048ecd748..79f538d5813 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -62,7 +62,7 @@ public void shouldReturnTargetingResult() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNotNull(); @@ -80,7 +80,7 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -94,7 +94,7 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -108,7 +108,7 @@ public void shouldNotFailWhenHttpClientIsCrashed() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -123,7 +123,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then assertThat(result.result()).isNull(); @@ -143,7 +143,7 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -163,7 +163,7 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { // when final Future result = target.getTargeting(null, "accountId", "origin", - "query", List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -186,7 +186,7 @@ public void shouldPassThroughIpAddresses() { "key", "accountId", "origin", - "query", + givenQuery(), List.of("8.8.8.8", "2001:4860:4860::8888"), 1000); @@ -207,7 +207,7 @@ public void shouldNotPassThroughIpAddressWhenNotSpecified() { // when final Future result = target.getTargeting("key", "accountId", "origin", - "query", null, 1000); + givenQuery(), null, 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); From ffdf319932b32b160e04c8a35a835811fac13eda Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 17 Apr 2025 15:05:04 +0200 Subject: [PATCH 14/41] optable-targeting: Increase module version --- extra/modules/optable-targeting/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index fa7d5cf73ac..9b1d83fce57 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.23.0-SNAPSHOT + 3.24.0-SNAPSHOT optable-targeting From 9c8ee5994b307af68f6107944db5f2fc18625ba8 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 17 Apr 2025 20:26:37 +0200 Subject: [PATCH 15/41] optable-targeting: Implement targeting invocation with IP and empty ids list --- .../optable/targeting/model/Query.java | 5 ++++ .../targeting/v1/core/QueryBuilder.java | 2 +- .../targeting/v1/core/QueryBuilderTest.java | 23 +++++++++++++++++-- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java index 20862050f39..ba46e55b7e5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.model; import lombok.Value; +import org.apache.commons.lang3.StringUtils; @Value(staticConstructor = "of") public class Query { @@ -10,6 +11,10 @@ public class Query { String attributes; public String toQueryString() { + if (StringUtils.isEmpty(ids) && ! StringUtils.isEmpty(attributes)) { + return attributes.substring(1); + } + return ids + attributes; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index fd3a411fde6..c0bd355a26b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -17,7 +17,7 @@ public class QueryBuilder { public Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { - if (CollectionUtils.isEmpty(ids)) { + if (CollectionUtils.isEmpty(ids) && CollectionUtils.isEmpty(optableAttributes.getIps())) { return null; } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 618c9bf1276..057c4c1fde3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -89,12 +89,31 @@ public void shouldBuildQueryStringWithRightOrder() { } @Test - public void shouldNotBuildQueryStringWhenIdsListIsEmpty() { + public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { // given final List ids = List.of(); + final OptableAttributes attributes = OptableAttributes.builder() + .ips(List.of("8.8.8.8")) + .build(); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final Query query = target.build(ids, attributes, idPrefixOrder); + + // then + + assertThat(query).isNotNull(); + assertThat(query.toQueryString()).isEqualTo("gdpr=0"); + } + + @Test + public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { + // given + final List ids = List.of(); + final OptableAttributes attributes = OptableAttributes.builder() + .build(); + + // when + final Query query = target.build(ids, attributes, idPrefixOrder); // then From eaf738d9b907880e4b80eaef32eb5112a6cf4263 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 17 Apr 2025 20:51:13 +0200 Subject: [PATCH 16/41] optable-targeting: Return targeting result even when caching fails --- .../modules/optable/targeting/v1/core/OptableTargeting.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 8866751f665..b1573a73bac 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -77,6 +77,8 @@ private Future fetchAndCacheResult(CachingKey cachingKey, Strin long timeout) { return apiClient.getTargeting(apiKey, tenant, origin, query, ips, timeout) - .compose(result -> cache.put(cachingKey.toString(), result, ttlSeconds).map(result)); + .compose(result -> cache.put(cachingKey.toString(), result, ttlSeconds) + .recover(throwable -> Future.succeededFuture()) + .map(result)); } } From 6e10e0e9583a370f786c33158ec670d862c3b097 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 8 May 2025 22:13:45 +0200 Subject: [PATCH 17/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 46 ++++++++++-------- .../optable/targeting/model/CachingKey.java | 36 -------------- .../targeting/model/EnrichmentStatus.java | 14 ++++-- .../optable/targeting/model/Metrics.java | 7 --- .../targeting/model/ModuleContext.java | 16 ++----- .../modules/optable/targeting/model/OS.java | 6 ++- .../optable/targeting/model/Query.java | 2 +- .../model/config/CacheProperties.java | 20 +++----- .../config/OptableTargetingProperties.java | 20 ++------ .../targeting/model/net/OptableError.java | 2 +- .../OptableTargetingAuctionResponseHook.java | 17 +++++-- ...eTargetingProcessedAuctionRequestHook.java | 13 ++--- .../v1/analytics/AnalyticTagsResolver.java | 7 +-- .../v1/analytics/AnalyticsTagsBuilder.java | 2 +- .../v1/core/AuctionResponseValidator.java | 5 +- .../v1/core/ExecutionTimeResolver.java | 37 ++++++++++++++ .../targeting/v1/core/IdsResolver.java | 2 +- .../targeting/v1/core/OptableTargeting.java | 26 +++++++--- .../targeting/model/CachingKeyTest.java | 48 ------------------- .../optable/targeting/v1/BaseOptableTest.java | 25 +++++----- ...tableTargetingAuctionResponseHookTest.java | 9 +++- .../v1/OptableTargetingModuleTest.java | 6 ++- .../v1/core/AuctionResponseValidatorTest.java | 12 ++--- 23 files changed, 161 insertions(+), 217 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 0a9094a42f0..158c22ea1ce 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.config; -import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; import org.prebid.server.cache.PbcStorageService; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; @@ -9,8 +8,9 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -40,23 +40,18 @@ public class OptableTargetingConfig { @Bean - ObjectMapper objectMapper() { - return ObjectMapperProvider.mapper(); - } - - @Bean - AnalyticTagsResolver analyticTagsResolver(ObjectMapper objectMapper) { - return new AnalyticTagsResolver(objectMapper); + AnalyticTagsResolver analyticTagsResolver() { + return new AnalyticTagsResolver(ObjectMapperProvider.mapper()); } @Bean - IdsMapper queryParametersExtractor(ObjectMapper objectMapper) { - return new IdsMapper(objectMapper); + IdsMapper queryParametersExtractor() { + return new IdsMapper(ObjectMapperProvider.mapper()); } @Bean - PayloadResolver payloadResolver(ObjectMapper mapper) { - return new PayloadResolver(mapper); + PayloadResolver payloadResolver() { + return new PayloadResolver(ObjectMapperProvider.mapper()); } @Bean @@ -109,13 +104,22 @@ OptableTargeting optableTargeting(IdsMapper parametersExtractor, @Value("${cache.module.enabled:false}") boolean moduleCacheEnabled) { - return new OptableTargeting(cache, parametersExtractor, queryBuilder, apiClient, + return new OptableTargeting( + cache, + parametersExtractor, + queryBuilder, + apiClient, storageEnabled && moduleCacheEnabled); } @Bean - ConfigResolver configResolver(ObjectMapper mapper, OptableTargetingProperties globalProperties) { - return new ConfigResolver(mapper, globalProperties); + ConfigResolver configResolver(OptableTargetingProperties globalProperties) { + return new ConfigResolver(ObjectMapperProvider.mapper(), globalProperties); + } + + @Bean + ExecutionTimeResolver executionTimeResolver() { + return new ExecutionTimeResolver(); } @Bean @@ -123,11 +127,13 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, AnalyticTagsResolver analyticTagsResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, - OptableAttributesResolver optableAttributesResolver) { + OptableAttributesResolver optableAttributesResolver, + ExecutionTimeResolver executionTimeResolver) { return new OptableTargetingModule(List.of( - new OptableTargetingProcessedAuctionRequestHook(configResolver, optableTargeting, payloadResolver, - optableAttributesResolver), - new OptableTargetingAuctionResponseHook(analyticTagsResolver, payloadResolver, configResolver))); + new OptableTargetingProcessedAuctionRequestHook( + configResolver, optableTargeting, payloadResolver, optableAttributesResolver), + new OptableTargetingAuctionResponseHook( + analyticTagsResolver, payloadResolver, configResolver, executionTimeResolver))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java deleted file mode 100644 index 114916663b7..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKey.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model; - -import lombok.AllArgsConstructor; -import org.apache.commons.collections4.CollectionUtils; - -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; - -@AllArgsConstructor(staticName = "of") -public class CachingKey { - - String tenant; - - String origin; - - Query query; - - List ips; - - public String toString() { - return "%s:%s:%s:%s".formatted( - tenant, - origin, - CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", - query.getIds()); - } - - public String toEncodedString() { - return "%s:%s:%s:%s".formatted( - tenant, - origin, - CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", - URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8)); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java index 97b81900558..e0a79970ecd 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java @@ -1,15 +1,19 @@ package org.prebid.server.hooks.modules.optable.targeting.model; -import lombok.Builder; +import lombok.Value; -@Builder(toBuilder = true) -public record EnrichmentStatus(Status status, Reason reason) { +@Value(staticConstructor = "of") +public class EnrichmentStatus { + + private final Status status; + + private final Reason reason; public static EnrichmentStatus failure() { - return EnrichmentStatus.builder().status(Status.FAIL).build(); + return EnrichmentStatus.of(Status.FAIL, null); } public static EnrichmentStatus success() { - return EnrichmentStatus.builder().status(Status.SUCCESS).build(); + return EnrichmentStatus.of(Status.SUCCESS, null); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java deleted file mode 100644 index 04788ecbc84..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Metrics.java +++ /dev/null @@ -1,7 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model; - -import lombok.Builder; - -@Builder(toBuilder = true) -public record Metrics(long moduleStartTime, long moduleFinishTime) { -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index 1f5eb064b73..4b6b7fc410a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -10,8 +10,6 @@ @Getter public class ModuleContext { - private Metrics metrics; - private List targeting; private EnrichmentStatus enrichRequestStatus; @@ -20,15 +18,12 @@ public class ModuleContext { private boolean adserverTargetingEnabled; + private long optableTargetingExecutionTime; + public static ModuleContext of(AuctionInvocationContext invocationContext) { return (ModuleContext) Objects.requireNonNull(invocationContext.moduleContext()); } - public ModuleContext setMetrics(Metrics metrics) { - this.metrics = metrics; - return this; - } - public ModuleContext setTargeting(List targeting) { this.targeting = targeting; return this; @@ -49,11 +44,8 @@ public ModuleContext setAdserverTargetingEnabled(boolean adserverTargetingEnable return this; } - public ModuleContext setFinishTime(long timestamp) { - setMetrics(getMetrics().toBuilder() - .moduleFinishTime(timestamp) - .build()); - + public ModuleContext setOptableTargetingExecutionTime(long optableTargetingExecutionTime) { + this.optableTargetingExecutionTime = optableTargetingExecutionTime; return this; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java index 7f2388f5614..8af2273115e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OS.java @@ -12,9 +12,13 @@ public enum OS { FIRE("fire"); - public final String value; + private final String value; OS(String value) { this.value = value; } + + public String getValue() { + return value; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java index ba46e55b7e5..54980f12c33 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -11,7 +11,7 @@ public class Query { String attributes; public String toQueryString() { - if (StringUtils.isEmpty(ids) && ! StringUtils.isEmpty(attributes)) { + if (StringUtils.isEmpty(ids) && !StringUtils.isEmpty(attributes)) { return attributes.substring(1); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java index 4bc3a21a48f..555c5b01277 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/CacheProperties.java @@ -1,21 +1,13 @@ package org.prebid.server.hooks.modules.optable.targeting.model.config; -import lombok.Getter; -import lombok.Setter; +import lombok.Data; +import lombok.NoArgsConstructor; -@Getter -@Setter +@Data +@NoArgsConstructor public class CacheProperties { - boolean enabled = false; + private boolean enabled = false; - int ttlseconds = 86400; - - public CacheProperties() { - } - - public CacheProperties(boolean enabled, int ttlseconds) { - this.enabled = enabled; - this.ttlseconds = ttlseconds; - } + private int ttlseconds = 86400; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index ed92779f4cf..3ba708d913c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; +import lombok.NoArgsConstructor; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -9,6 +10,7 @@ @ConfigurationProperties(prefix = "hooks.modules." + OptableTargetingModule.CODE) @Data +@NoArgsConstructor public final class OptableTargetingProperties { @JsonProperty("api-endpoint") @@ -32,21 +34,5 @@ public final class OptableTargetingProperties { @JsonProperty("id-prefix-order") String idPrefixOrder; - public OptableTargetingProperties() { - } - - public OptableTargetingProperties(String apiEndpoint, String apiKey, Map ppidMapping, - Boolean adserverTargeting, Long timeout, String idPrefixOrder, - CacheProperties cache) { - - this.apiEndpoint = apiEndpoint; - this.apiKey = apiKey; - this.ppidMapping = ppidMapping; - this.adserverTargeting = adserverTargeting; - this.timeout = timeout; - this.idPrefixOrder = idPrefixOrder; - this.cache = cache; - } - - CacheProperties cache = new CacheProperties(false, 86400); + CacheProperties cache = new CacheProperties(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java index 9f97eb6bf7d..1dc5338c882 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java @@ -23,7 +23,7 @@ public enum Type { private final Integer code; - Type(final Integer errorCode) { + Type(Integer errorCode) { this.code = errorCode; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 5f4c93b450d..f2b5cbf3de0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -12,17 +12,18 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; import java.util.List; import java.util.Objects; -import java.util.function.Function; public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { @@ -34,14 +35,18 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook private final ConfigResolver configResolver; + private final ExecutionTimeResolver executionTimeResolver; + public OptableTargetingAuctionResponseHook( AnalyticTagsResolver analyticTagsResolver, PayloadResolver payloadResolver, - ConfigResolver configResolver) { + ConfigResolver configResolver, + ExecutionTimeResolver executionTimeResolver) { this.analyticTagsResolver = Objects.requireNonNull(analyticTagsResolver); this.payloadResolver = Objects.requireNonNull(payloadResolver); this.configResolver = configResolver; + this.executionTimeResolver = executionTimeResolver; } @Override @@ -53,13 +58,15 @@ public Future> call(AuctionResponsePayl final ModuleContext moduleContext = ModuleContext.of(invocationContext); moduleContext.setAdserverTargetingEnabled(adserverTargeting); + moduleContext.setOptableTargetingExecutionTime( + executionTimeResolver.extractOptableTargetingExecutionTime(invocationContext)); if (adserverTargeting) { final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); moduleContext.setEnrichResponseStatus(validationStatus); - if (validationStatus.status() == Status.SUCCESS) { + if (validationStatus.getStatus() == Status.SUCCESS) { return enrichedPayload(moduleContext); } } @@ -80,13 +87,13 @@ private AuctionResponsePayload enrichPayload(AuctionResponsePayload payload, Lis } private Future> update( - Function func, ModuleContext moduleContext) { + PayloadUpdate payloadUpdate, ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) - .payloadUpdate(func::apply) + .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) .analyticsTags(analyticTagsResolver.resolve(moduleContext)) .build()); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 2043eadf721..4a3049c2b39 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -6,7 +6,6 @@ import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; -import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; @@ -28,7 +27,7 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { - private static final String CODE = "optable-targeting-processed-auction-request-hook"; + public static final String CODE = "optable-targeting-processed-auction-request-hook"; private static final long DEFAULT_API_CALL_TIMEOUT = 1000L; @@ -56,10 +55,7 @@ public Future> call(AuctionRequestPayloa AuctionInvocationContext invocationContext) { final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); - final ModuleContext moduleContext = new ModuleContext() - .setMetrics(Metrics.builder() - .moduleStartTime(System.currentTimeMillis()) - .build()); + final ModuleContext moduleContext = new ModuleContext(); final BidRequest bidRequest = getBidRequest(auctionRequestPayload); if (bidRequest == null) { @@ -105,8 +101,6 @@ private long getHookRemainingTime(AuctionInvocationContext invocationContext) { private Future> enrichedPayload(TargetingResult targetingResult, ModuleContext moduleContext) { - moduleContext.setFinishTime(System.currentTimeMillis()); - if (targetingResult != null) { moduleContext.setTargeting(targetingResult.getAudience()) .setEnrichRequestStatus(EnrichmentStatus.success()); @@ -144,8 +138,7 @@ private static Future> update( private static Future> failure(ModuleContext moduleContext) { return success(moduleContext - .setEnrichRequestStatus(EnrichmentStatus.failure()) - .setFinishTime(System.currentTimeMillis())); + .setEnrichRequestStatus(EnrichmentStatus.failure())); } private static Future> failure( diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java index e95c14b3293..51798cc81b7 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java @@ -1,7 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; import com.fasterxml.jackson.databind.ObjectMapper; -import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.v1.analytics.Tags; @@ -16,14 +15,12 @@ public AnalyticTagsResolver(ObjectMapper objectMapper) { } public Tags resolve(ModuleContext moduleContext) { - final Metrics metrics = moduleContext.getMetrics(); - return AnalyticsTagsBuilder.builder() .objectMapper(objectMapper) .adserverTargeting(moduleContext.isAdserverTargetingEnabled()) - .requestEnrichmentStatus(moduleContext.getEnrichRequestStatus().status()) + .requestEnrichmentStatus(moduleContext.getEnrichRequestStatus().getStatus()) .responseEnrichmentStatus(moduleContext.getEnrichResponseStatus()) - .requestEnrichmentExecutionTime(metrics.moduleFinishTime() - metrics.moduleStartTime()) + .requestEnrichmentExecutionTime(moduleContext.getOptableTargetingExecutionTime()) .build() .buildTags(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java index 93a01eb5452..f8a5ce391af 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java @@ -61,7 +61,7 @@ private Activity buildEnrichRequestActivity() { private Activity buildEnrichResponseActivity() { final String activityStatus = requestEnrichmentStatus != null ? requestEnrichmentStatus.getValue() : null; final String enrichmentStatus = responseEnrichmentStatus != null - ? responseEnrichmentStatus.reason().getValue() + ? responseEnrichmentStatus.getReason().getValue() : null; final Result activityResult = buildResult(STATUS_REASON, enrichmentStatus); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java index 660d0429017..79e81691899 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java @@ -28,10 +28,7 @@ public static EnrichmentStatus checkEnrichmentPossibility(BidResponse bidRespons reason = Reason.NOBID; } - return EnrichmentStatus.builder() - .status(status) - .reason(reason) - .build(); + return EnrichmentStatus.of(status, reason); } private static boolean hasKeywords(List targeting) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java new file mode 100644 index 00000000000..77a6b1194cf --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.prebid.server.auction.model.AuctionContext; +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.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; + +import java.util.Collection; +import java.util.Optional; + +public class ExecutionTimeResolver { + + public long extractOptableTargetingExecutionTime(AuctionInvocationContext invocationContext) { + return Optional.ofNullable(invocationContext.auctionContext()) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getStageOutcomes) + .map(stages -> stages.get(Stage.processed_auction_request)) + .stream() + .flatMap(Collection::stream) + .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hook -> OptableTargetingModule.CODE.equals(hook.getHookId().getModuleCode())) + .filter(hook -> + OptableTargetingProcessedAuctionRequestHook.CODE.equals(hook.getHookId().getHookImplCode())) + .findFirst() + .map(HookExecutionOutcome::getExecutionTime) + .orElse(0L); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java index 4559cf8b1e1..87f0e56ba03 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java @@ -59,7 +59,7 @@ public String getDeviceIfa(OS os) { return null; } - if (deviceOS.contains(os.value.toLowerCase()) && !(deviceLmt != null && deviceLmt.equals(1))) { + if (deviceOS.contains(os.getValue().toLowerCase()) && !(deviceLmt != null && deviceLmt.equals(1))) { return device.map(Device::getIfa).orElse(null); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index b1573a73bac..b3f0c47f7ec 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -2,7 +2,7 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; -import org.prebid.server.hooks.modules.optable.targeting.model.CachingKey; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; @@ -10,6 +10,8 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -61,24 +63,34 @@ private Future getOrFetchTargetingResults(CacheProperties cache String tenant, String origin, Query query, List ips, long timeout) { - final CachingKey cachingKey = CachingKey.of(tenant, origin, query, ips); - - return cache.get(cachingKey.toEncodedString()) + final String cachingKey = createCachingKey(tenant, origin, ips, query, true); + return cache.get(cachingKey) .recover(err -> Future.succeededFuture(null)) .compose(entry -> entry != null ? Future.succeededFuture(entry) - : fetchAndCacheResult(cachingKey, tenant, origin, cacheProperties.getTtlseconds(), apiKey, + : fetchAndCacheResult(tenant, origin, cacheProperties.getTtlseconds(), apiKey, query, ips, timeout)); } - private Future fetchAndCacheResult(CachingKey cachingKey, String tenant, String origin, + private Future fetchAndCacheResult(String tenant, String origin, int ttlSeconds, String apiKey, Query query, List ips, long timeout) { + final String cachingKey = createCachingKey(tenant, origin, ips, query, false); return apiClient.getTargeting(apiKey, tenant, origin, query, ips, timeout) - .compose(result -> cache.put(cachingKey.toString(), result, ttlSeconds) + .compose(result -> cache.put(cachingKey, result, ttlSeconds) .recover(throwable -> Future.succeededFuture()) .map(result)); } + + private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { + return "%s:%s:%s:%s".formatted( + tenant, + origin, + CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", + encodeQuery + ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) + : query.getIds()); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java deleted file mode 100644 index 2d167610c67..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/model/CachingKeyTest.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model; - -import org.assertj.core.api.Assertions; -import org.junit.jupiter.api.Test; - -import java.util.List; - -public class CachingKeyTest { - - @Test - public void shouldBuildCachingUrlEncodedKey() { - // given - final Query query = Query.of("query?String", "&attributes"); - final CachingKey cachingKey = CachingKey.of("tenant", "origin", query, List.of("8.8.8.8")); - - // when - final String key = cachingKey.toEncodedString(); - - // then - Assertions.assertThat(key).isEqualTo("tenant:origin:8.8.8.8:query%3FString"); - } - - @Test - public void shouldBuildCachingKey() { - // given - final Query query = Query.of("query?String", "&attributes"); - final CachingKey cachingKey = CachingKey.of("tenant", "origin", query, List.of("8.8.8.8")); - - // when - final String key = cachingKey.toString(); - - // then - Assertions.assertThat(key).isEqualTo("tenant:origin:8.8.8.8:query?String"); - } - - @Test - public void shouldPutNoneIfNoIpAddress() { - // given - final Query query = Query.of("query?String", "&attributes"); - final CachingKey cachingKey = CachingKey.of("tenant", "origin", query, null); - - // when - final String key = cachingKey.toString(); - - // then - Assertions.assertThat(key).isEqualTo("tenant:origin:none:query?String"); - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 0349cd9534d..abea98704e5 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -20,7 +20,6 @@ import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; -import org.prebid.server.hooks.modules.optable.targeting.model.Metrics; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; @@ -54,9 +53,6 @@ protected ModuleContext givenModuleContext() { protected ModuleContext givenModuleContext(List audiences) { final ModuleContext moduleContext = new ModuleContext(); - moduleContext.setMetrics(Metrics.builder() - .moduleStartTime(System.currentTimeMillis()) - .build()); moduleContext.setTargeting(audiences); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); @@ -174,15 +170,18 @@ protected String givenBodyFromFile(String fileName) { } protected OptableTargetingProperties givenOptableTargetingProperties(boolean enableCache) { - return new OptableTargetingProperties( - "endpoint", - "key", - Map.of("c", "id"), - true, - 100L, - null, - new CacheProperties(enableCache, 86400) - ); + final CacheProperties cacheProperties = new CacheProperties(); + cacheProperties.setEnabled(enableCache); + + final OptableTargetingProperties optableTargetingProperties = new OptableTargetingProperties(); + optableTargetingProperties.setApiEndpoint("endpoint"); + optableTargetingProperties.setApiKey("key"); + optableTargetingProperties.setPpidMapping(Map.of("c", "id")); + optableTargetingProperties.setAdserverTargeting(true); + optableTargetingProperties.setTimeout(100L); + optableTargetingProperties.setCache(cacheProperties); + + return optableTargetingProperties; } protected Query givenQuery() { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 3908e5113f2..973439a09fa 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -15,6 +15,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -47,6 +48,8 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { private ConfigResolver configResolver; + private ExecutionTimeResolver executionTimeResolver = new ExecutionTimeResolver(); + @BeforeEach public void setUp() { when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); @@ -55,7 +58,8 @@ public void setUp() { target = new OptableTargetingAuctionResponseHook( new AnalyticTagsResolver(mapper), payloadResolver, - configResolver); + configResolver, + executionTimeResolver); } @Test @@ -132,7 +136,8 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { target = new OptableTargetingAuctionResponseHook( new AnalyticTagsResolver(mapper), payloadResolver, - configResolver); + configResolver, + executionTimeResolver); // when final Future> future = target.call(auctionResponsePayload, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 052ff8490d2..7567065cd08 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -6,6 +6,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -36,6 +37,8 @@ public class OptableTargetingModuleTest { @Mock AnalyticTagsResolver analyticTagsResolver; + ExecutionTimeResolver executionTimeResolver = new ExecutionTimeResolver(); + @Test public void shouldReturnNonBlankCode() { // given @@ -60,7 +63,8 @@ public void shouldReturnHooks() { new OptableTargetingAuctionResponseHook( analyticTagsResolver, payloadResolver, - configResolver)); + configResolver, + executionTimeResolver)); final Module module = new OptableTargetingModule(hooks); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java index 40a6cfd9e75..a46e59b1974 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java @@ -28,8 +28,8 @@ public void shouldReturnNobidStatusWhenBidResponseIsEmpty() { // then assertThat(result).isNotNull() - .returns(Status.FAIL, EnrichmentStatus::status) - .returns(Reason.NOBID, EnrichmentStatus::reason); + .returns(Status.FAIL, EnrichmentStatus::getStatus) + .returns(Reason.NOBID, EnrichmentStatus::getReason); } @Test @@ -44,8 +44,8 @@ public void shouldReturnNoKeywordsStatusWhenTargetingHasNoIds() { // then assertThat(result).isNotNull() - .returns(Status.FAIL, EnrichmentStatus::status) - .returns(Reason.NOBID, EnrichmentStatus::reason); + .returns(Status.FAIL, EnrichmentStatus::getStatus) + .returns(Reason.NOBID, EnrichmentStatus::getReason); } @Test @@ -64,8 +64,8 @@ public void shouldReturnSuccessStatus() { // then assertThat(result).isNotNull() - .returns(Status.SUCCESS, EnrichmentStatus::status) - .returns(Reason.NONE, EnrichmentStatus::reason); + .returns(Status.SUCCESS, EnrichmentStatus::getStatus) + .returns(Reason.NONE, EnrichmentStatus::getReason); } protected List givenTargeting() { From 043874f79e12ccca2a4373a0d26cdedd07fa8673 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 12 May 2025 21:59:07 +0200 Subject: [PATCH 18/41] optable-targeting: Mask first-party user data when transfer is not permitted --- .../config/OptableTargetingConfig.java | 15 ++++-- ...eTargetingProcessedAuctionRequestHook.java | 50 ++++++++++++++++--- .../optable/targeting/v1/BaseOptableTest.java | 4 +- .../v1/OptableTargetingModuleTest.java | 13 ++++- ...getingProcessedAuctionRequestHookTest.java | 14 +++++- 5 files changed, 83 insertions(+), 13 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 158c22ea1ce..a4fc5ba59a9 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.config; import io.vertx.core.Vertx; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.cache.PbcStorageService; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; @@ -128,12 +129,20 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver, - ExecutionTimeResolver executionTimeResolver) { + ExecutionTimeResolver executionTimeResolver, + UserFpdActivityMask userFpdActivityMask) { return new OptableTargetingModule(List.of( new OptableTargetingProcessedAuctionRequestHook( - configResolver, optableTargeting, payloadResolver, optableAttributesResolver), + configResolver, + optableTargeting, + payloadResolver, + optableAttributesResolver, + userFpdActivityMask), new OptableTargetingAuctionResponseHook( - analyticTagsResolver, payloadResolver, configResolver, executionTimeResolver))); + analyticTagsResolver, + payloadResolver, + configResolver, + executionTimeResolver))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 4a3049c2b39..ac6696c9920 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -1,7 +1,16 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.User; import io.vertx.core.Future; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; +import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -39,15 +48,19 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final OptableAttributesResolver optableAttributesResolver; + private final UserFpdActivityMask userFpdActivityMask; + public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, - OptableAttributesResolver optableAttributesResolver) { + OptableAttributesResolver optableAttributesResolver, + UserFpdActivityMask userFpdActivityMask) { this.configResolver = Objects.requireNonNull(configResolver); this.optableTargeting = Objects.requireNonNull(optableTargeting); this.payloadResolver = Objects.requireNonNull(payloadResolver); this.optableAttributesResolver = Objects.requireNonNull(optableAttributesResolver); + this.userFpdActivityMask = userFpdActivityMask; } @Override @@ -57,10 +70,11 @@ public Future> call(AuctionRequestPayloa final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); final ModuleContext moduleContext = new ModuleContext(); - final BidRequest bidRequest = getBidRequest(auctionRequestPayload); + BidRequest bidRequest = auctionRequestPayload.bidRequest(); if (bidRequest == null) { return failure(moduleContext); } + bidRequest = applyActivityRestrictions(bidRequest, invocationContext); final long timeout = getHookRemainingTime(invocationContext); final OptableAttributes attributes = optableAttributesResolver.resolveAttributes( @@ -85,10 +99,34 @@ public Future> call(AuctionRequestPayloa moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()))); } - private BidRequest getBidRequest(AuctionRequestPayload auctionRequestPayload) { - return Optional.ofNullable(auctionRequestPayload) - .map(AuctionRequestPayload::bidRequest) - .orElse(null); + private BidRequest applyActivityRestrictions(BidRequest bidRequest, + AuctionInvocationContext auctionInvocationContext) { + + final boolean disallowTransmitUfpd = !isTransmitUfpdAllowed(bidRequest, auctionInvocationContext); + + return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd); + } + + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, boolean disallowTransmitUfpd) { + final User maskedUser = userFpdActivityMask.maskUser( + bidRequest.getUser(), disallowTransmitUfpd, false); + final Device maskedDevice = userFpdActivityMask.maskDevice( + bidRequest.getDevice(), disallowTransmitUfpd, false); + + return bidRequest.toBuilder() + .user(maskedUser) + .device(maskedDevice) + .build(); + } + + private boolean isTransmitUfpdAllowed(BidRequest bidRequest, AuctionInvocationContext auctionInvocationContext) { + final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), + bidRequest); + + return auctionContext.getActivityInfrastructure() + .isAllowed(Activity.TRANSMIT_UFPD, activityInvocationPayload); } private long getHookRemainingTime(AuctionInvocationContext invocationContext) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index abea98704e5..12a471027b9 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -16,6 +16,7 @@ import io.vertx.core.http.impl.headers.HeadersMultiMap; import org.apache.commons.io.IOUtils; import org.apache.http.HttpStatus; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.execution.timeout.Timeout; @@ -59,9 +60,10 @@ protected ModuleContext givenModuleContext(List audiences) { return moduleContext; } - protected AuctionContext givenAuctionContext(Timeout timeout) { + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) { return AuctionContext.builder() .bidRequest(givenBidRequest()) + .activityInfrastructure(activityInfrastructure) .timeoutContext(TimeoutContext.of(0, timeout, 1)) .build(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 7567065cd08..3f782b7bedb 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; @@ -18,6 +19,10 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mock.Strictness.LENIENT; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class OptableTargetingModuleTest { @@ -37,6 +42,9 @@ public class OptableTargetingModuleTest { @Mock AnalyticTagsResolver analyticTagsResolver; + @Mock(strictness = LENIENT) + UserFpdActivityMask userFpdActivityMask; + ExecutionTimeResolver executionTimeResolver = new ExecutionTimeResolver(); @Test @@ -54,12 +62,15 @@ public void shouldReturnNonBlankCode() { @Test public void shouldReturnHooks() { // given + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); final Collection> hooks = List.of(new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, payloadResolver, - optableAttributesResolver), + optableAttributesResolver, + userFpdActivityMask), new OptableTargetingAuctionResponseHook( analyticTagsResolver, payloadResolver, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 5258d14cdec..5191b4f9e73 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -10,6 +10,8 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -24,6 +26,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; @@ -39,6 +42,10 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable AuctionInvocationContext invocationContext; @Mock Timeout timeout; + @Mock + UserFpdActivityMask userFpdActivityMask; + @Mock + ActivityInfrastructure activityInfrastructure; private ConfigResolver configResolver; @@ -50,14 +57,17 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @BeforeEach public void setUp() { + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); when(timeout.remaining()).thenReturn(1000L); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); - when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(timeout)); + when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); configResolver = new ConfigResolver(mapper, givenOptableTargetingProperties(false)); payloadResolver = new PayloadResolver(mapper); optableAttributesResolver = new OptableAttributesResolver(); target = new OptableTargetingProcessedAuctionRequestHook(configResolver, optableTargeting, - payloadResolver, optableAttributesResolver); + payloadResolver, optableAttributesResolver, userFpdActivityMask); } @Test From 0e6edc0f8f9b5d1cc3bccf054adb7fcb496eb88e Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Tue, 13 May 2025 20:41:42 +0200 Subject: [PATCH 19/41] optable-targeting: up pbs version --- extra/modules/optable-targeting/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index 9b1d83fce57..e633ee189d1 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.24.0-SNAPSHOT + 3.26.0-SNAPSHOT optable-targeting From ddca0bb2eb64981ced9b6403857862780b9a81ef Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sat, 24 May 2025 16:08:58 +0200 Subject: [PATCH 20/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 30 ++--- .../targeting/model/EnrichmentStatus.java | 4 +- .../targeting/model/ModuleContext.java | 33 +---- .../config/OptableTargetingProperties.java | 3 - .../targeting/model/net/HttpRequest.java | 2 +- .../targeting/model/openrtb/Audience.java | 2 - .../OptableTargetingAuctionResponseHook.java | 75 +++++++++-- ...eTargetingProcessedAuctionRequestHook.java | 74 +++++------ .../v1/analytics/AnalyticTagsResolver.java | 27 ---- .../v1/analytics/AnalyticsTagsBuilder.java | 87 ------------- .../v1/core/AuctionResponseValidator.java | 28 ++-- .../optable/targeting/v1/core/Cache.java | 5 +- .../targeting/v1/core/ConfigResolver.java | 18 ++- .../targeting/v1/core/EndpointResolver.java | 16 --- .../optable/targeting/v1/core/IdsMapper.java | 123 ++++++++++++------ .../targeting/v1/core/IdsResolver.java | 118 ----------------- .../optable/targeting/v1/net/APIClient.java | 17 +-- ...tableTargetingAuctionResponseHookTest.java | 18 ++- .../v1/OptableTargetingModuleTest.java | 12 +- ...getingProcessedAuctionRequestHookTest.java | 32 +---- .../targeting/v1/net/APIClientTest.java | 4 +- 21 files changed, 260 insertions(+), 468 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index a4fc5ba59a9..9dd8cddf92a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.config; +import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.cache.PbcStorageService; @@ -7,7 +8,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; -import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; @@ -20,29 +20,28 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.spring.config.VertxContextScope; import org.prebid.server.spring.config.model.HttpClientProperties; -import org.prebid.server.util.HttpUtil; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.ScopedProxyMode; import java.util.List; -import java.util.Objects; @ConditionalOnProperty(prefix = "hooks." + OptableTargetingModule.CODE, name = "enabled", havingValue = "true") @Configuration -@EnableConfigurationProperties(OptableTargetingProperties.class) public class OptableTargetingConfig { @Bean - AnalyticTagsResolver analyticTagsResolver() { - return new AnalyticTagsResolver(ObjectMapperProvider.mapper()); + @ConfigurationProperties(prefix = "hooks.modules." + OptableTargetingModule.CODE) + OptableTargetingProperties properties() { + return new OptableTargetingProperties(); } @Bean @@ -80,9 +79,11 @@ APIClient apiClient(OptableHttpClientWrapper httpClientWrapper, OptableTargetingProperties properties, OptableResponseMapper responseParser) { - final String endpoint = HttpUtil.validateUrl(Objects.requireNonNull(properties.getApiEndpoint())); - - return new APIClient(endpoint, httpClientWrapper.getHttpClient(), logSamplingRate, responseParser); + return new APIClient( + properties.getApiEndpoint(), + httpClientWrapper.getHttpClient(), + logSamplingRate, + responseParser); } @Bean @@ -114,8 +115,8 @@ OptableTargeting optableTargeting(IdsMapper parametersExtractor, } @Bean - ConfigResolver configResolver(OptableTargetingProperties globalProperties) { - return new ConfigResolver(ObjectMapperProvider.mapper(), globalProperties); + ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties globalProperties) { + return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties); } @Bean @@ -125,7 +126,6 @@ ExecutionTimeResolver executionTimeResolver() { @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, - AnalyticTagsResolver analyticTagsResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver, @@ -140,9 +140,9 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, optableAttributesResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( - analyticTagsResolver, payloadResolver, configResolver, - executionTimeResolver))); + executionTimeResolver, + ObjectMapperProvider.mapper()))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java index e0a79970ecd..e0dc94d34cf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/EnrichmentStatus.java @@ -5,9 +5,9 @@ @Value(staticConstructor = "of") public class EnrichmentStatus { - private final Status status; + Status status; - private final Reason reason; + Reason reason; public static EnrichmentStatus failure() { return EnrichmentStatus.of(Status.FAIL, null); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index 4b6b7fc410a..ed0264f0249 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -1,13 +1,12 @@ package org.prebid.server.hooks.modules.optable.targeting.model; -import lombok.Getter; +import lombok.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import java.util.List; -import java.util.Objects; -@Getter +@Data public class ModuleContext { private List targeting; @@ -21,31 +20,7 @@ public class ModuleContext { private long optableTargetingExecutionTime; public static ModuleContext of(AuctionInvocationContext invocationContext) { - return (ModuleContext) Objects.requireNonNull(invocationContext.moduleContext()); - } - - public ModuleContext setTargeting(List targeting) { - this.targeting = targeting; - return this; - } - - public ModuleContext setEnrichRequestStatus(EnrichmentStatus enrichRequestStatus) { - this.enrichRequestStatus = enrichRequestStatus; - return this; - } - - public ModuleContext setEnrichResponseStatus(EnrichmentStatus enrichResponseStatus) { - this.enrichResponseStatus = enrichResponseStatus; - return this; - } - - public ModuleContext setAdserverTargetingEnabled(boolean adserverTargetingEnabled) { - this.adserverTargetingEnabled = adserverTargetingEnabled; - return this; - } - - public ModuleContext setOptableTargetingExecutionTime(long optableTargetingExecutionTime) { - this.optableTargetingExecutionTime = optableTargetingExecutionTime; - return this; + final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); + return moduleContext != null ? moduleContext : new ModuleContext(); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 3ba708d913c..c13bf132ca9 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -3,12 +3,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; -import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; -import org.springframework.boot.context.properties.ConfigurationProperties; import java.util.Map; -@ConfigurationProperties(prefix = "hooks.modules." + OptableTargetingModule.CODE) @Data @NoArgsConstructor public final class OptableTargetingProperties { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java index b9188bb1027..90782e05041 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java @@ -4,7 +4,7 @@ import lombok.Builder; import lombok.Value; -@Builder(toBuilder = true) +@Builder @Value public class HttpRequest { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java index 8d793c5d5d7..566c46c2c35 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Audience.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; -import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import java.util.List; @@ -14,6 +13,5 @@ public class Audience { String keyspace; - @JsonProperty("rtb_segtax") Integer rtbSegtax; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index f2b5cbf3de0..9dbf385c639 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -1,15 +1,20 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; 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.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; -import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; @@ -18,35 +23,42 @@ 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.Result; +import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Objects; +import java.util.Optional; public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { private static final String CODE = "optable-targeting-auction-response-hook"; - - private final AnalyticTagsResolver analyticTagsResolver; + private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; + private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; + private static final String STATUS_EXECUTION_TIME = "execution-time"; + private static final String STATUS_REASON = "reason"; private final PayloadResolver payloadResolver; - private final ConfigResolver configResolver; - private final ExecutionTimeResolver executionTimeResolver; + private final ObjectMapper objectMapper; public OptableTargetingAuctionResponseHook( - AnalyticTagsResolver analyticTagsResolver, PayloadResolver payloadResolver, ConfigResolver configResolver, - ExecutionTimeResolver executionTimeResolver) { + ExecutionTimeResolver executionTimeResolver, + ObjectMapper objectMapper) { - this.analyticTagsResolver = Objects.requireNonNull(analyticTagsResolver); this.payloadResolver = Objects.requireNonNull(payloadResolver); - this.configResolver = configResolver; - this.executionTimeResolver = executionTimeResolver; + this.configResolver = Objects.requireNonNull(configResolver); + this.executionTimeResolver = Objects.requireNonNull(executionTimeResolver); + this.objectMapper = Objects.requireNonNull(objectMapper); } @Override @@ -95,7 +107,7 @@ private Future> update( .action(InvocationAction.update) .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) - .analyticsTags(analyticTagsResolver.resolve(moduleContext)) + .analyticsTags(toAnalyticTags(moduleContext)) .build()); } @@ -105,10 +117,49 @@ private Future> success(ModuleContext m .status(InvocationStatus.success) .action(InvocationAction.no_action) .moduleContext(moduleContext) - .analyticsTags(analyticTagsResolver.resolve(moduleContext)) + .analyticsTags(toAnalyticTags(moduleContext)) .build()); } + private Tags toAnalyticTags(ModuleContext moduleContext) { + final String requestEnrichmentStatus = toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()); + final EnrichmentStatus responseEnrichmentStatus = moduleContext.getEnrichResponseStatus(); + final String responseEnrichmentStatusValue = toEnrichmentStatusValue(responseEnrichmentStatus); + final String responseEnrichmentStatusReason = toEnrichmentStatusReason(moduleContext.getEnrichResponseStatus()); + + final List activities = new ArrayList<>(); + activities.add(ActivityImpl.of(ACTIVITY_ENRICH_REQUEST, + requestEnrichmentStatus, + toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime())))); + + if (moduleContext.isAdserverTargetingEnabled()) { + activities.add(ActivityImpl.of(ACTIVITY_ENRICH_RESPONSE, + responseEnrichmentStatusValue, + toResults(STATUS_REASON, responseEnrichmentStatusReason))); + } + + return TagsImpl.of(activities); + } + + private String toEnrichmentStatusValue(EnrichmentStatus enrichRequestStatus) { + return Optional.ofNullable(enrichRequestStatus) + .map(EnrichmentStatus::getStatus) + .map(Status::getValue) + .orElse(null); + } + + private String toEnrichmentStatusReason(EnrichmentStatus enrichmentStatus) { + return Optional.ofNullable(enrichmentStatus) + .map(EnrichmentStatus::getReason) + .map(Reason::getValue) + .orElse(null); + } + + private List toResults(String result, String value) { + final ObjectNode resultDetails = objectMapper.createObjectNode().put(result, value); + return Collections.singletonList(ResultImpl.of(null, resultDetails, null)); + } + @Override public String code() { return CODE; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index ac6696c9920..b33cde01c05 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -6,6 +6,7 @@ import io.vertx.core.Future; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; @@ -26,6 +27,7 @@ import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; @@ -37,17 +39,11 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { public static final String CODE = "optable-targeting-processed-auction-request-hook"; - private static final long DEFAULT_API_CALL_TIMEOUT = 1000L; - private final ConfigResolver configResolver; - private final OptableTargeting optableTargeting; - private final PayloadResolver payloadResolver; - private final OptableAttributesResolver optableAttributesResolver; - private final UserFpdActivityMask userFpdActivityMask; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, @@ -60,7 +56,7 @@ public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver this.optableTargeting = Objects.requireNonNull(optableTargeting); this.payloadResolver = Objects.requireNonNull(payloadResolver); this.optableAttributesResolver = Objects.requireNonNull(optableAttributesResolver); - this.userFpdActivityMask = userFpdActivityMask; + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } @Override @@ -70,11 +66,7 @@ public Future> call(AuctionRequestPayloa final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); final ModuleContext moduleContext = new ModuleContext(); - BidRequest bidRequest = auctionRequestPayload.bidRequest(); - if (bidRequest == null) { - return failure(moduleContext); - } - bidRequest = applyActivityRestrictions(bidRequest, invocationContext); + final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); final long timeout = getHookRemainingTime(invocationContext); final OptableAttributes attributes = optableAttributesResolver.resolveAttributes( @@ -88,30 +80,46 @@ public Future> call(AuctionRequestPayloa timeout); if (targetingResultFuture == null) { - return failure( - this::sanitizePayload, - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure())); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + return failure(this::sanitizePayload, moduleContext); } return targetingResultFuture.compose(targetingResult -> enrichedPayload(targetingResult, moduleContext)) - .recover(throwable -> failure( - this::sanitizePayload, - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()))); + .recover(throwable -> { + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + return failure(this::sanitizePayload, moduleContext); + }); } private BidRequest applyActivityRestrictions(BidRequest bidRequest, AuctionInvocationContext auctionInvocationContext) { + if (bidRequest == null) { + return null; + } + + final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), + bidRequest); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); - final boolean disallowTransmitUfpd = !isTransmitUfpdAllowed(bidRequest, auctionInvocationContext); + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed(Activity.TRANSMIT_UFPD, + activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed(Activity.TRANSMIT_EIDS, + activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed(Activity.TRANSMIT_GEO, + activityInvocationPayload); - return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd); + return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); } - private BidRequest maskUserPersonalInfo(BidRequest bidRequest, boolean disallowTransmitUfpd) { + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, boolean disallowTransmitUfpd, + boolean disallowTransmitEids, boolean disallowTransmitGeo) { + final User maskedUser = userFpdActivityMask.maskUser( - bidRequest.getUser(), disallowTransmitUfpd, false); + bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); final Device maskedDevice = userFpdActivityMask.maskDevice( - bidRequest.getDevice(), disallowTransmitUfpd, false); + bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); return bidRequest.toBuilder() .user(maskedUser) @@ -119,16 +127,6 @@ private BidRequest maskUserPersonalInfo(BidRequest bidRequest, boolean disallowT .build(); } - private boolean isTransmitUfpdAllowed(BidRequest bidRequest, AuctionInvocationContext auctionInvocationContext) { - final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); - final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( - ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), - bidRequest); - - return auctionContext.getActivityInfrastructure() - .isAllowed(Activity.TRANSMIT_UFPD, activityInvocationPayload); - } - private long getHookRemainingTime(AuctionInvocationContext invocationContext) { return Optional.ofNullable(invocationContext) .map(AuctionInvocationContext::timeout) @@ -140,8 +138,8 @@ private Future> enrichedPayload(Targetin ModuleContext moduleContext) { if (targetingResult != null) { - moduleContext.setTargeting(targetingResult.getAudience()) - .setEnrichRequestStatus(EnrichmentStatus.success()); + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); return update(payload -> { final AuctionRequestPayload sanitizedPayload = sanitizePayload(payload); @@ -162,7 +160,7 @@ private AuctionRequestPayload sanitizePayload(AuctionRequestPayload payload) { } private static Future> update( - Function func, + PayloadUpdate func, ModuleContext moduleContext) { return Future.succeededFuture( @@ -175,8 +173,8 @@ private static Future> update( } private static Future> failure(ModuleContext moduleContext) { - return success(moduleContext - .setEnrichRequestStatus(EnrichmentStatus.failure())); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + return success(moduleContext); } private static Future> failure( diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java deleted file mode 100644 index 51798cc81b7..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticTagsResolver.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; - -import com.fasterxml.jackson.databind.ObjectMapper; -import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.Objects; - -public class AnalyticTagsResolver { - - private final ObjectMapper objectMapper; - - public AnalyticTagsResolver(ObjectMapper objectMapper) { - this.objectMapper = Objects.requireNonNull(objectMapper); - } - - public Tags resolve(ModuleContext moduleContext) { - return AnalyticsTagsBuilder.builder() - .objectMapper(objectMapper) - .adserverTargeting(moduleContext.isAdserverTargetingEnabled()) - .requestEnrichmentStatus(moduleContext.getEnrichRequestStatus().getStatus()) - .responseEnrichmentStatus(moduleContext.getEnrichResponseStatus()) - .requestEnrichmentExecutionTime(moduleContext.getOptableTargetingExecutionTime()) - .build() - .buildTags(); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java deleted file mode 100644 index f8a5ce391af..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/analytics/AnalyticsTagsBuilder.java +++ /dev/null @@ -1,87 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.analytics; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Builder; -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.modules.optable.targeting.model.EnrichmentStatus; -import org.prebid.server.hooks.modules.optable.targeting.model.Status; -import org.prebid.server.hooks.v1.analytics.Activity; -import org.prebid.server.hooks.v1.analytics.Result; -import org.prebid.server.hooks.v1.analytics.Tags; - -import java.util.ArrayList; -import java.util.List; - -@Builder(toBuilder = true) -public class AnalyticsTagsBuilder { - - private static final String STATUS_OK = "OK"; - - private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; - - private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; - - private static final String STATUS_EXECUTION_TIME = "execution-time"; - - private static final String STATUS_REASON = "reason"; - - private final ObjectMapper objectMapper; - - private final long requestEnrichmentExecutionTime; - - private final Status requestEnrichmentStatus; - - private final EnrichmentStatus responseEnrichmentStatus; - - private final boolean adserverTargeting; - - public Tags buildTags() { - final List activities = new ArrayList<>(); - activities.add(buildEnrichRequestActivity()); - - if (adserverTargeting) { - activities.add(buildEnrichResponseActivity()); - } - return TagsImpl.of(activities); - } - - private Activity buildEnrichRequestActivity() { - final String activityStatus = requestEnrichmentStatus != null ? requestEnrichmentStatus.getValue() : null; - final Result activityResult = buildResult(STATUS_EXECUTION_TIME, requestEnrichmentExecutionTime); - - return ActivityImpl.of( - ACTIVITY_ENRICH_REQUEST, - activityStatus, - List.of(activityResult)); - } - - private Activity buildEnrichResponseActivity() { - final String activityStatus = requestEnrichmentStatus != null ? requestEnrichmentStatus.getValue() : null; - final String enrichmentStatus = responseEnrichmentStatus != null - ? responseEnrichmentStatus.getReason().getValue() - : null; - final Result activityResult = buildResult(STATUS_REASON, enrichmentStatus); - - return ActivityImpl.of( - ACTIVITY_ENRICH_RESPONSE, - activityStatus, - List.of(activityResult)); - } - - private Result buildResult(String resultName, String resultValue) { - final ObjectNode resultObj = objectMapper.createObjectNode() - .put(resultName, resultValue); - - return ResultImpl.of(null, resultObj, null); - } - - private Result buildResult(String resultName, Long resultValue) { - final ObjectNode resultObj = objectMapper.createObjectNode() - .put(resultName, resultValue); - - return ResultImpl.of(null, resultObj, null); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java index 79e81691899..44b22f8b92e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidator.java @@ -9,6 +9,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import java.util.List; +import java.util.Objects; import java.util.Optional; public class AuctionResponseValidator { @@ -17,18 +18,13 @@ private AuctionResponseValidator() { } public static EnrichmentStatus checkEnrichmentPossibility(BidResponse bidResponse, List targeting) { - Status status = Status.SUCCESS; - Reason reason = Reason.NONE; - if (!hasKeywords(targeting)) { - status = Status.FAIL; - reason = Reason.NOKEYWORD; + return EnrichmentStatus.of(Status.FAIL, Reason.NOKEYWORD); } else if (!hasBids(bidResponse)) { - status = Status.FAIL; - reason = Reason.NOBID; + return EnrichmentStatus.of(Status.FAIL, Reason.NOBID); } - return EnrichmentStatus.of(status, reason); + return EnrichmentStatus.of(Status.SUCCESS, Reason.NONE); } private static boolean hasKeywords(List targeting) { @@ -36,11 +32,9 @@ private static boolean hasKeywords(List targeting) { return false; } - final long idsCounter = targeting.stream() - .mapToLong(audience -> Optional.ofNullable(audience.getIds()).orElse(List.of()).size()) - .sum(); - - return idsCounter > 0; + return targeting.stream() + .filter(Objects::nonNull) + .anyMatch(audience -> CollectionUtils.isNotEmpty(audience.getIds())); } private static boolean hasBids(BidResponse bidResponse) { @@ -49,10 +43,8 @@ private static boolean hasBids(BidResponse bidResponse) { return false; } - final long bidsCount = seatBids.stream() - .mapToLong(seatBid -> Optional.ofNullable(seatBid.getBid()).orElse(List.of()).size()) - .sum(); - - return bidsCount > 0; + return seatBids.stream() + .filter(Objects::nonNull) + .anyMatch(seatBid -> CollectionUtils.isNotEmpty(seatBid.getBid())); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index 5c3b14ef240..b7304ea0bf3 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -30,7 +30,7 @@ public Future get(String query) { return cacheService.retrieveEntry(query, APP_CODE, APPLICATION) .map(ModuleCacheResponse::getValue) .map(it -> it != null ? optableResponseMapper.parse(it) : null) - .otherwise(it -> null); + .otherwiseEmpty(); } public Future put(String query, TargetingResult value, int ttlSeconds) { @@ -43,6 +43,7 @@ public Future put(String query, TargetingResult value, int ttlSeconds) { optableResponseMapper.toJsonString(value), StorageDataType.TEXT, ttlSeconds, - APPLICATION, APP_CODE); + APPLICATION, + APP_CODE); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java index f4750901e1e..219f37af2ab 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ConfigResolver.java @@ -1,28 +1,34 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.json.JsonMerger; +import java.util.Objects; import java.util.Optional; public class ConfigResolver { private final ObjectMapper mapper; - + private final JsonMerger jsonMerger; private final OptableTargetingProperties globalProperties; + private final JsonNode globalPropertiesObjectNode; - public ConfigResolver(ObjectMapper mapper, OptableTargetingProperties globalProperties) { - this.mapper = mapper; - this.globalProperties = globalProperties; + public ConfigResolver(ObjectMapper mapper, JsonMerger jsonMerger, OptableTargetingProperties globalProperties) { + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + this.globalProperties = Objects.requireNonNull(globalProperties); + this.globalPropertiesObjectNode = Objects.requireNonNull(mapper.valueToTree(globalProperties)); } public OptableTargetingProperties resolve(ObjectNode configNode) { - final ObjectNode mergedNode = ((ObjectNode) mapper.valueToTree(globalProperties)).setAll(configNode); + final JsonNode mergedNode = jsonMerger.merge(configNode, globalPropertiesObjectNode); return parse(mergedNode).orElse(globalProperties); } - private Optional parse(ObjectNode configNode) { + private Optional parse(JsonNode configNode) { try { return Optional.ofNullable(configNode) .filter(node -> !node.isEmpty()) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java deleted file mode 100644 index 021334ce3c8..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/EndpointResolver.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -public class EndpointResolver { - - private static final String TENANT = "{TENANT}"; - - private static final String ORIGIN = "{ORIGIN}"; - - private EndpointResolver() { - } - - public static String resolve(String endpointTemplate, String tenant, String origin) { - return endpointTemplate.replace(TENANT, tenant) - .replace(ORIGIN, origin); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index 5c226444270..ab7f4eb31d0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -1,22 +1,34 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.tuple.Pair; +import org.apache.commons.collections4.MapUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OS; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; -import java.util.stream.Collectors; +import java.util.Optional; public class IdsMapper { + private static final Map STATIC_PPID_MAPPING = Map.of( + "id5-sync.com", Id.ID5, + "utiq.com", Id.UTIQ, + "netid.de", Id.NET_ID); + private final ObjectMapper objectMapper; public IdsMapper(ObjectMapper objectMapper) { @@ -24,58 +36,91 @@ public IdsMapper(ObjectMapper objectMapper) { } public List toIds(BidRequest bidRequest, Map ppidMapping) { - final IdsResolver idsResolver = IdsResolver.of(objectMapper, bidRequest); + final User user = bidRequest.getUser(); - final Map ids = applyStaticMapping(idsResolver); - final Map dynamicIds = applyDynamicMapping(idsResolver, ppidMapping); - if (dynamicIds != null && !dynamicIds.isEmpty()) { - ids.putAll(dynamicIds); - } + final Map ids = new HashMap<>(); + addOptableIds(ids, user); + addDeviceIds(ids, bidRequest.getDevice()); + addEidsIds(ids, user, STATIC_PPID_MAPPING); + addEidsIds(ids, user, ppidMapping); return ids.entrySet().stream() - .filter(it -> it.getValue() != null) .map(it -> Id.of(it.getKey(), it.getValue())) .toList(); } - private Map toIds(List eids, Map ppidMapping) { - if (ppidMapping == null || ppidMapping.isEmpty() || CollectionUtils.isEmpty(eids)) { - return null; - } + private void addOptableIds(Map ids, User user) { + final Optional extUserOptable = Optional.ofNullable(user) + .map(User::getExt) + .map(ext -> ext.getProperty("optable")) + .map(this::parseExtUserOptable); - return eids.stream() - .map(eid -> eidSourceToFirstUidId(eid, ppidMapping)) - .filter(Objects::nonNull) - .collect(Collectors.toMap(Pair::getLeft, Pair::getRight)); + extUserOptable.map(ExtUserOptable::getEmail).ifPresent(it -> ids.put(Id.EMAIL, it)); + extUserOptable.map(ExtUserOptable::getPhone).ifPresent(it -> ids.put(Id.PHONE, it)); + extUserOptable.map(ExtUserOptable::getZip).ifPresent(it -> ids.put(Id.ZIP, it)); + extUserOptable.map(ExtUserOptable::getVid).ifPresent(it -> ids.put(Id.OPTABLE_VID, it)); } - private Pair eidSourceToFirstUidId(Eid eid, Map ppidMapping) { - final String key = ppidMapping.get(eid.getSource()); - - return key != null ? Pair.of(key, getFirstUidId(eid)) : null; + private ExtUserOptable parseExtUserOptable(JsonNode node) { + try { + return objectMapper.treeToValue(node, ExtUserOptable.class); + } catch (JsonProcessingException e) { + // TODO: you need to handle this exception in some way + throw new RuntimeException(e); + } } - private String getFirstUidId(Eid eid) { - return eid.getUids().stream().findFirst().map(Uid::getId).orElse(null); + private static void addDeviceIds(Map ids, Device device) { + final String ifa = device != null ? device.getIfa() : null; + final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null; + final int lmt = Optional.ofNullable(device).map(Device::getLmt).orElse(0); + + if (ifa == null || StringUtils.isEmpty(os) || lmt == 1) { + return; + } + + if (os.contains(OS.IOS.getValue())) { + ids.put(Id.APPLE_IDFA, ifa); + } + if (os.contains(OS.ANDROID.getValue())) { + ids.put(Id.GOOGLE_GAID, ifa); + } + if (os.contains(OS.ROKU.getValue())) { + ids.put(Id.ROKU_RIDA, ifa); + } + if (os.contains(OS.TIZEN.getValue())) { + ids.put(Id.SAMSUNG_TV_TIFA, ifa); + } + if (os.contains(OS.FIRE.getValue())) { + ids.put(Id.AMAZON_FIRE_AFAI, ifa); + } } - private Map applyDynamicMapping(IdsResolver idsResolver, Map ppidMapping) { - return toIds(idsResolver.getEIDs(), ppidMapping); + private static void addEidsIds(Map ids, User user, Map ppidMapping) { + final List eids = user != null ? user.getEids() : null; + if (MapUtils.isEmpty(ppidMapping) || CollectionUtils.isEmpty(eids)) { + return; + } + + for (Eid eid : eids) { + final String source = eid != null ? eid.getSource() : null; + if (source == null) { + continue; + } + + final String idKey = ppidMapping.get(source); + if (idKey != null) { + firstUidId(eid).ifPresent(it -> ids.put(idKey, it)); + } + } } - private Map applyStaticMapping(IdsResolver idsResolver) { - return new HashMap<>() {{ - put(Id.EMAIL, idsResolver.getEmail()); - put(Id.PHONE, idsResolver.getPhone()); - put(Id.ZIP, idsResolver.getZip()); - put(Id.APPLE_IDFA, idsResolver.getDeviceIfa(OS.IOS)); - put(Id.GOOGLE_GAID, idsResolver.getDeviceIfa(OS.ANDROID)); - put(Id.ROKU_RIDA, idsResolver.getDeviceIfa(OS.ROKU)); - put(Id.SAMSUNG_TV_TIFA, idsResolver.getDeviceIfa(OS.TIZEN)); - put(Id.AMAZON_FIRE_AFAI, idsResolver.getDeviceIfa(OS.FIRE)); - put(Id.NET_ID, idsResolver.getNetId()); - put(Id.ID5, idsResolver.getID5()); - put(Id.UTIQ, idsResolver.getUtiq()); - put(Id.OPTABLE_VID, idsResolver.getOptableVID()); }}; + private static Optional firstUidId(Eid eid) { + return Optional.ofNullable(eid.getUids()) + .orElse(Collections.emptyList()) + .stream() + .filter(Objects::nonNull) + .findFirst() + .map(Uid::getId); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java deleted file mode 100644 index 87f0e56ba03..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsResolver.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.Uid; -import com.iab.openrtb.request.User; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.hooks.modules.optable.targeting.model.OS; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; - -import java.util.List; -import java.util.Optional; - -public class IdsResolver { - - private final Optional bidRequest; - private final ObjectMapper objectMapper; - private final Optional extUser; - private final Optional extUserOptable; - private final Optional device; - - public static IdsResolver of(ObjectMapper objectMapper, BidRequest bidRequest) { - return new IdsResolver(objectMapper, bidRequest); - } - - private IdsResolver(ObjectMapper objectMapper, BidRequest bidRequest) { - this.objectMapper = objectMapper; - this.bidRequest = Optional.ofNullable(bidRequest); - this.extUser = getExtUser(); - this.extUserOptable = getExtUserOptable(); - this.device = this.bidRequest.map(BidRequest::getDevice); - } - - public String getEmail() { - return extUserOptable.map(ExtUserOptable::getEmail).orElse(null); - } - - public String getPhone() { - return extUserOptable.map(ExtUserOptable::getPhone).orElse(null); - } - - public String getZip() { - return extUserOptable.map(ExtUserOptable::getZip).orElse(null); - } - - public String getOptableVID() { - return extUserOptable.map(ExtUserOptable::getVid).orElse(null); - } - - public String getDeviceIfa(OS os) { - final String deviceOS = getDeviceOS(); - final Integer deviceLmt = getDeviceLmt(); - - if (StringUtils.isEmpty(deviceOS)) { - return null; - } - - if (deviceOS.contains(os.getValue().toLowerCase()) && !(deviceLmt != null && deviceLmt.equals(1))) { - return device.map(Device::getIfa).orElse(null); - } - - return null; - } - - public String getID5() { - return getUid("id5-sync.com"); - } - - public String getUtiq() { - return getUid("utiq.com"); - } - - public String getNetId() { - return getUid("netid.de"); - } - - public List getEIDs() { - return bidRequest.map(BidRequest::getUser).map(User::getEids).orElse(List.of()); - } - - private Optional getExtUser() { - return bidRequest.map(BidRequest::getUser).map(User::getExt); - } - - private Integer getDeviceLmt() { - return device.map(Device::getLmt).orElse(null); - } - - private String getDeviceOS() { - return device.map(Device::getOs).map(String::toLowerCase).orElse(null); - } - - private String getUid(String source) { - return getEIDs() - .stream() - .filter(it -> it.getSource().equals(source)) - .findFirst() - .map(Eid::getUids) - .flatMap(it -> it.stream().findFirst()) - .map(Uid::getId) - .orElse(null); - } - - private Optional getExtUserOptable() { - return extUser.map(it -> it.getProperty("optable")) - .map(it -> { - try { - return objectMapper.treeToValue(it, ExtUserOptable.class); - } catch (JsonProcessingException e) { - throw new RuntimeException(e); - } - }); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index c6c826813ea..29369db9e2f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -14,7 +14,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableError; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.EndpointResolver; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; @@ -29,16 +28,13 @@ public class APIClient { + private static final String TENANT = "{TENANT}"; + private static final String ORIGIN = "{ORIGIN}"; private static final Logger logger = LoggerFactory.getLogger(APIClient.class); - private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); - private final String endpoint; - private final HttpClient httpClient; - private final double logSamplingRate; - private final OptableResponseMapper responseMapper; public APIClient(String endpoint, @@ -46,7 +42,7 @@ public APIClient(String endpoint, double logSamplingRate, OptableResponseMapper responseMapper) { - this.endpoint = Objects.requireNonNull(endpoint); + this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); this.httpClient = Objects.requireNonNull(httpClient); this.logSamplingRate = logSamplingRate; this.responseMapper = Objects.requireNonNull(responseMapper); @@ -69,7 +65,7 @@ public Future getTargeting(String apiKey, String tenant, String } final HttpRequest request = HttpRequest.builder() - .uri(EndpointResolver.resolve(endpoint, tenant, origin)) + .uri(resolveEndpoint(endpoint, tenant, origin)) .query(query.toQueryString()) .headers(headers) .build(); @@ -77,6 +73,11 @@ public Future getTargeting(String apiKey, String tenant, String return doRequest(request, timeout); } + private String resolveEndpoint(String endpointTemplate, String tenant, String origin) { + return endpointTemplate.replace(TENANT, tenant) + .replace(ORIGIN, origin); + } + private Future doRequest(HttpRequest httpRequest, long timeout) { return createRequest(httpRequest, timeout) .compose(response -> processResponse(response, httpRequest)) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 973439a09fa..199f9fef8ce 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -12,7 +12,6 @@ import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; @@ -23,6 +22,8 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import java.util.List; @@ -37,13 +38,10 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { AuctionResponsePayload auctionResponsePayload; @Mock(strictness = LENIENT) AuctionInvocationContext invocationContext; - private PayloadResolver payloadResolver; - private AuctionResponseValidator auctionResponseValidator; - private final ObjectMapper mapper = new ObjectMapper(); - + private final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); private AuctionResponseHook target; private ConfigResolver configResolver; @@ -54,12 +52,12 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { public void setUp() { when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); payloadResolver = new PayloadResolver(mapper); - configResolver = new ConfigResolver(mapper, givenOptableTargetingProperties(false)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); target = new OptableTargetingAuctionResponseHook( - new AnalyticTagsResolver(mapper), payloadResolver, configResolver, - executionTimeResolver); + executionTimeResolver, + mapper); } @Test @@ -134,10 +132,10 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", List.of(new AudienceId("audienceId")), "keyspace", 1)))); target = new OptableTargetingAuctionResponseHook( - new AnalyticTagsResolver(mapper), payloadResolver, configResolver, - executionTimeResolver); + executionTimeResolver, + mapper); // when final Future> future = target.call(auctionResponsePayload, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 3f782b7bedb..9ff0122895d 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -1,11 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; -import org.prebid.server.hooks.modules.optable.targeting.v1.analytics.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; @@ -14,6 +14,7 @@ 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.json.ObjectMapperProvider; import java.util.Collection; import java.util.List; @@ -39,14 +40,13 @@ public class OptableTargetingModuleTest { @Mock OptableAttributesResolver optableAttributesResolver; - @Mock - AnalyticTagsResolver analyticTagsResolver; - @Mock(strictness = LENIENT) UserFpdActivityMask userFpdActivityMask; ExecutionTimeResolver executionTimeResolver = new ExecutionTimeResolver(); + ObjectMapper mapper = ObjectMapperProvider.mapper(); + @Test public void shouldReturnNonBlankCode() { // given @@ -72,10 +72,10 @@ public void shouldReturnHooks() { optableAttributesResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( - analyticTagsResolver, payloadResolver, configResolver, - executionTimeResolver)); + executionTimeResolver, + mapper)); final Module module = new OptableTargetingModule(hooks); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 5191b4f9e73..77cc4438f30 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; @@ -23,6 +24,8 @@ import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -46,13 +49,10 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable UserFpdActivityMask userFpdActivityMask; @Mock ActivityInfrastructure activityInfrastructure; - private ConfigResolver configResolver; - private PayloadResolver payloadResolver; - private OptableAttributesResolver optableAttributesResolver; - + private JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(new ObjectMapper())); private OptableTargetingProcessedAuctionRequestHook target; @BeforeEach @@ -63,7 +63,7 @@ public void setUp() { when(timeout.remaining()).thenReturn(1000L); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); - configResolver = new ConfigResolver(mapper, givenOptableTargetingProperties(false)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); payloadResolver = new PayloadResolver(mapper); optableAttributesResolver = new OptableAttributesResolver(); target = new OptableTargetingProcessedAuctionRequestHook(configResolver, optableTargeting, @@ -135,28 +135,6 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() { assertThat(optable).isNull(); } - @Test - public void shouldReturnResultWithoutUpdateActionWhenBidRequestIsNull() { - // given - when(auctionRequestPayload.bidRequest()).thenReturn(null); - when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) - .thenReturn(Future.succeededFuture(givenTargetingResult())); - - // when - final Future> future = target.call(auctionRequestPayload, - invocationContext); - - // then - assertThat(future).isNotNull(); - assertThat(future.succeeded()).isTrue(); - - final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.no_action); - assertThat(result.errors()).isNull(); - } - @Test public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() { // given diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index 79f538d5813..5c6b4b94051 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -48,7 +48,7 @@ public class APIClientTest extends BaseOptableTest { @BeforeEach public void setUp() { - target = new APIClient("endpoint", + target = new APIClient("http://endpoint.optable.com", httpClient, LOG_SAMPLING_RATE, parser); @@ -132,7 +132,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { @Test public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // given - target = new APIClient("endpoint", + target = new APIClient("http://endpoint.optable.com", httpClient, LOG_SAMPLING_RATE, parser); From 8d13b5e6a2f20b046c451a9d964f9ed0422e618d Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sun, 25 May 2025 18:44:01 +0200 Subject: [PATCH 21/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 1 - .../targeting/model/net/OptableCall.java | 10 ++--- .../optable/targeting/v1/core/Cache.java | 2 - .../optable/targeting/v1/core/IpResolver.java | 37 ------------------- .../v1/core/OptableAttributesResolver.java | 26 ++++++++++++- .../targeting/v1/core/OptableTargeting.java | 2 +- .../v1/core/merger/BidResponseResolver.java | 1 - .../optable/targeting/v1/net/APIClient.java | 24 +----------- .../v1/net/OptableResponseMapperTest.java | 8 ++-- 9 files changed, 36 insertions(+), 75 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 9dd8cddf92a..fe8bbded2a5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.config; -import com.fasterxml.jackson.databind.ObjectMapper; import io.vertx.core.Vertx; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.cache.PbcStorageService; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java index 37cca225dca..bd3e98a3470 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java @@ -12,13 +12,11 @@ public class OptableCall { HttpResponse response; - OptableError error; - - public static OptableCall succeededHttp(HttpRequest request, HttpResponse response, OptableError error) { - return new OptableCall(request, response, error); + public static OptableCall succeededHttp(HttpRequest request, HttpResponse response) { + return new OptableCall(request, response); } - public static OptableCall failedHttp(HttpRequest request, OptableError error) { - return new OptableCall(request, null, error); + public static OptableCall failedHttp(HttpRequest request) { + return new OptableCall(request, null); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index b7304ea0bf3..a0973acc7de 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -12,11 +12,9 @@ public class Cache { private static final String APP_CODE = "prebid-Java"; - private static final String APPLICATION = "optable-targeting"; private final PbcStorageService cacheService; - private final OptableResponseMapper optableResponseMapper; public Cache(PbcStorageService cacheService, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java deleted file mode 100644 index 6be859d0253..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IpResolver.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.privacy.model.PrivacyContext; - -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - -public class IpResolver { - - private IpResolver() { - } - - public static List resolveIp(AuctionContext auctionContext) { - final List result = new ArrayList<>(); - - final Optional auctionContextOpt = Optional.ofNullable(auctionContext); - - final Optional deviceOpt = auctionContextOpt - .map(AuctionContext::getBidRequest) - .map(BidRequest::getDevice); - - deviceOpt.map(Device::getIp).ifPresent(result::add); - deviceOpt.map(Device::getIpv6).ifPresent(result::add); - - if (result.isEmpty()) { - auctionContextOpt.map(AuctionContext::getPrivacyContext) - .map(PrivacyContext::getIpAddress) - .ifPresent(result::add); - } - - return result; - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 93676d9f538..ff63105917f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,12 +1,15 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.iab.gpp.encoder.GppModel; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.PrivacyContext; +import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Optional; @@ -15,7 +18,7 @@ public class OptableAttributesResolver { public OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { - final List ips = IpResolver.resolveIp(auctionContext); + final List ips = resolveIp(auctionContext); return Optional.ofNullable(getGdprPrivacyAttributes(auctionContext)) .or(() -> Optional.ofNullable(getGppPrivacyAttributes(auctionContext))) @@ -26,6 +29,27 @@ public OptableAttributes resolveAttributes(AuctionContext auctionContext, Long t .build(); } + public static List resolveIp(AuctionContext auctionContext) { + final List result = new ArrayList<>(); + + final Optional auctionContextOpt = Optional.ofNullable(auctionContext); + + final Optional deviceOpt = auctionContextOpt + .map(AuctionContext::getBidRequest) + .map(BidRequest::getDevice); + + deviceOpt.map(Device::getIp).ifPresent(result::add); + deviceOpt.map(Device::getIpv6).ifPresent(result::add); + + if (result.isEmpty()) { + auctionContextOpt.map(AuctionContext::getPrivacyContext) + .map(PrivacyContext::getIpAddress) + .ifPresent(result::add); + } + + return result; + } + private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) { final Optional gppContextOpt = Optional.ofNullable(auctionContext) .map(AuctionContext::getGppContext); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index b3f0c47f7ec..14c94d23000 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -24,7 +24,7 @@ public class OptableTargeting { public OptableTargeting(Cache cache, IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient, boolean moduleCacheEnabled) { - this.cache = cache; + this.cache = Objects.requireNonNull(cache); this.idsMapper = Objects.requireNonNull(idsMapper); this.queryBuilder = Objects.requireNonNull(queryBuilder); this.apiClient = Objects.requireNonNull(apiClient); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java index 85933b58a36..04ad62f739f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java @@ -20,7 +20,6 @@ public class BidResponseResolver { private BidResponse bidResponse; - private ObjectMapper mapper; public BidResponse applyTargeting(List targeting) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index 29369db9e2f..de6da70a226 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; -import io.netty.channel.ConnectTimeoutException; import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.MultiMap; @@ -12,7 +11,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; -import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableError; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; @@ -24,7 +22,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.concurrent.TimeoutException; public class APIClient { @@ -122,18 +119,7 @@ private static Future processResponse(HttpClientResponse response, final int statusCode = response.getStatusCode(); final HttpResponse httpResponse = HttpResponse.of(statusCode, response.getHeaders(), response.getBody()); - return Future.succeededFuture(OptableCall.succeededHttp(httpRequest, httpResponse, errorOrNull(statusCode))); - } - - private static OptableError errorOrNull(int statusCode) { - if (statusCode != HttpResponseStatus.OK.code() && statusCode != HttpResponseStatus.NO_CONTENT.code()) { - return OptableError.of( - "Unexpected status code: %s.".formatted(statusCode), - statusCode == HttpResponseStatus.BAD_REQUEST.code() - ? OptableError.Type.BAD_INPUT - : OptableError.Type.BAD_SERVER_RESPONSE); - } - return null; + return Future.succeededFuture(OptableCall.succeededHttp(httpRequest, httpResponse)); } private Future failResponse(Throwable exception, HttpRequest httpRequest) { @@ -142,13 +128,7 @@ private Future failResponse(Throwable exception, HttpRequest httpRe logger.debug("Error occurred while sending HTTP request to the Optable url: {}", exception, httpRequest.getUri()); - final OptableError.Type errorType = - exception instanceof TimeoutException || exception instanceof ConnectTimeoutException - ? OptableError.Type.TIMEOUT - : OptableError.Type.GENERIC; - - return Future.succeededFuture( - OptableCall.failedHttp(httpRequest, OptableError.of(exception.getMessage(), errorType))); + return Future.succeededFuture(OptableCall.failedHttp(httpRequest)); } private OptableCall validateResponse(OptableCall response) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java index c130a4c3af5..dae577dbf8e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java @@ -38,7 +38,7 @@ public void shouldNotFailWhenSourceIsNull() { @Test public void shouldNotFailWhenResponseIsNull() { // given - final OptableCall optableCall = OptableCall.succeededHttp(null, null, null); + final OptableCall optableCall = OptableCall.succeededHttp(null, null); //when final TargetingResult result = target.parse(optableCall); @@ -51,7 +51,7 @@ public void shouldNotFailWhenResponseIsNull() { public void shouldNotFailWhenResponseBodyIsWrong() { // given final HttpResponse response = givenSuccessResponse("{\"field'\": \"value\"}"); - final OptableCall optableCall = OptableCall.succeededHttp(null, response, null); + final OptableCall optableCall = OptableCall.succeededHttp(null, response); //when final TargetingResult result = target.parse(optableCall); @@ -65,7 +65,7 @@ public void shouldNotFailWhenResponseBodyIsWrong() { public void shouldParseRightResponse() { // given final HttpResponse response = givenSuccessResponse(givenBodyFromFile("targeting_response.json")); - final OptableCall optableCall = OptableCall.succeededHttp(null, response, null); + final OptableCall optableCall = OptableCall.succeededHttp(null, response); //when final TargetingResult result = target.parse(optableCall); @@ -81,7 +81,7 @@ public void shouldParseRightResponse() { public void shouldFailWhenGotNotJsonString() { // given final HttpResponse response = givenSuccessResponse("random string"); - final OptableCall optableCall = OptableCall.succeededHttp(null, response, null); + final OptableCall optableCall = OptableCall.succeededHttp(null, response); //when and then assertThrows( From 20cd2c2e63224e324dc69eadd312662c30bb5ed1 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 26 May 2025 20:06:24 +0200 Subject: [PATCH 22/41] optable-targeting: remove redundant class: executionTimeResolver --- .../config/OptableTargetingConfig.java | 8 ---- .../OptableTargetingAuctionResponseHook.java | 34 ++++++++++++++--- .../v1/core/ExecutionTimeResolver.java | 37 ------------------- ...tableTargetingAuctionResponseHookTest.java | 5 --- .../v1/OptableTargetingModuleTest.java | 4 -- 5 files changed, 28 insertions(+), 60 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index fe8bbded2a5..5e4890b7886 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -9,7 +9,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; @@ -118,17 +117,11 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties); } - @Bean - ExecutionTimeResolver executionTimeResolver() { - return new ExecutionTimeResolver(); - } - @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver, - ExecutionTimeResolver executionTimeResolver, UserFpdActivityMask userFpdActivityMask) { return new OptableTargetingModule(List.of( @@ -141,7 +134,6 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, new OptableTargetingAuctionResponseHook( payloadResolver, configResolver, - executionTimeResolver, ObjectMapperProvider.mapper()))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 9dbf385c639..0ed5b0fa126 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -4,6 +4,12 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.auction.model.AuctionContext; +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.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; 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; @@ -17,7 +23,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -31,6 +36,7 @@ import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Objects; @@ -46,18 +52,15 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook private final PayloadResolver payloadResolver; private final ConfigResolver configResolver; - private final ExecutionTimeResolver executionTimeResolver; private final ObjectMapper objectMapper; public OptableTargetingAuctionResponseHook( PayloadResolver payloadResolver, ConfigResolver configResolver, - ExecutionTimeResolver executionTimeResolver, ObjectMapper objectMapper) { this.payloadResolver = Objects.requireNonNull(payloadResolver); this.configResolver = Objects.requireNonNull(configResolver); - this.executionTimeResolver = Objects.requireNonNull(executionTimeResolver); this.objectMapper = Objects.requireNonNull(objectMapper); } @@ -70,8 +73,7 @@ public Future> call(AuctionResponsePayl final ModuleContext moduleContext = ModuleContext.of(invocationContext); moduleContext.setAdserverTargetingEnabled(adserverTargeting); - moduleContext.setOptableTargetingExecutionTime( - executionTimeResolver.extractOptableTargetingExecutionTime(invocationContext)); + moduleContext.setOptableTargetingExecutionTime(extractOptableTargetingExecutionTime(invocationContext)); if (adserverTargeting) { final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( @@ -86,6 +88,26 @@ public Future> call(AuctionResponsePayl return success(moduleContext); } + private long extractOptableTargetingExecutionTime(AuctionInvocationContext invocationContext) { + return Optional.ofNullable(invocationContext.auctionContext()) + .map(AuctionContext::getHookExecutionContext) + .map(HookExecutionContext::getStageOutcomes) + .map(stages -> stages.get(Stage.processed_auction_request)) + .stream() + .flatMap(Collection::stream) + .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) + .map(StageExecutionOutcome::getGroups) + .flatMap(Collection::stream) + .map(GroupExecutionOutcome::getHooks) + .flatMap(Collection::stream) + .filter(hook -> OptableTargetingModule.CODE.equals(hook.getHookId().getModuleCode())) + .filter(hook -> + OptableTargetingProcessedAuctionRequestHook.CODE.equals(hook.getHookId().getHookImplCode())) + .findFirst() + .map(HookExecutionOutcome::getExecutionTime) + .orElse(0L); + } + private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java deleted file mode 100644 index 77a6b1194cf..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExecutionTimeResolver.java +++ /dev/null @@ -1,37 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -import org.prebid.server.auction.model.AuctionContext; -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.Stage; -import org.prebid.server.hooks.execution.model.StageExecutionOutcome; -import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; -import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; -import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; - -import java.util.Collection; -import java.util.Optional; - -public class ExecutionTimeResolver { - - public long extractOptableTargetingExecutionTime(AuctionInvocationContext invocationContext) { - return Optional.ofNullable(invocationContext.auctionContext()) - .map(AuctionContext::getHookExecutionContext) - .map(HookExecutionContext::getStageOutcomes) - .map(stages -> stages.get(Stage.processed_auction_request)) - .stream() - .flatMap(Collection::stream) - .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .filter(hook -> OptableTargetingModule.CODE.equals(hook.getHookId().getModuleCode())) - .filter(hook -> - OptableTargetingProcessedAuctionRequestHook.CODE.equals(hook.getHookId().getHookImplCode())) - .findFirst() - .map(HookExecutionOutcome::getExecutionTime) - .orElse(0L); - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 199f9fef8ce..3c8f1913d5a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -14,7 +14,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -46,8 +45,6 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { private ConfigResolver configResolver; - private ExecutionTimeResolver executionTimeResolver = new ExecutionTimeResolver(); - @BeforeEach public void setUp() { when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); @@ -56,7 +53,6 @@ public void setUp() { target = new OptableTargetingAuctionResponseHook( payloadResolver, configResolver, - executionTimeResolver, mapper); } @@ -134,7 +130,6 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { target = new OptableTargetingAuctionResponseHook( payloadResolver, configResolver, - executionTimeResolver, mapper); // when diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 9ff0122895d..3e3839ab704 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -7,7 +7,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.ExecutionTimeResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; @@ -43,8 +42,6 @@ public class OptableTargetingModuleTest { @Mock(strictness = LENIENT) UserFpdActivityMask userFpdActivityMask; - ExecutionTimeResolver executionTimeResolver = new ExecutionTimeResolver(); - ObjectMapper mapper = ObjectMapperProvider.mapper(); @Test @@ -74,7 +71,6 @@ public void shouldReturnHooks() { new OptableTargetingAuctionResponseHook( payloadResolver, configResolver, - executionTimeResolver, mapper)); final Module module = new OptableTargetingModule(hooks); From fda79af43bd715aa8923e5359e3afbb17f430c6a Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 2 Jun 2025 18:25:21 +0200 Subject: [PATCH 23/41] optable-targeting: up PBS version --- extra/modules/optable-targeting/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index e633ee189d1..34726f24487 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.26.0-SNAPSHOT + 3.27.0-SNAPSHOT optable-targeting From 026b99cbc0187a2ddc0bf77d20120f78deba4284 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 9 Jun 2025 23:11:09 +0200 Subject: [PATCH 24/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 4 +- .../targeting/model/net/OptableError.java | 30 --------- .../OptableTargetingAuctionResponseHook.java | 18 +++--- ...eTargetingProcessedAuctionRequestHook.java | 64 +++++-------------- .../optable/targeting/v1/core/IdsMapper.java | 13 +++- .../v1/core/OptableAttributesResolver.java | 5 ++ .../targeting/v1/core/OptableTargeting.java | 30 +++++---- ...getingProcessedAuctionRequestHookTest.java | 3 +- .../targeting/v1/core/IdMapperTest.java | 2 +- 9 files changed, 65 insertions(+), 104 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 5e4890b7886..1e858c60645 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -43,8 +43,8 @@ OptableTargetingProperties properties() { } @Bean - IdsMapper queryParametersExtractor() { - return new IdsMapper(ObjectMapperProvider.mapper()); + IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + return new IdsMapper(ObjectMapperProvider.mapper(), logSamplingRate); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java deleted file mode 100644 index 1dc5338c882..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableError.java +++ /dev/null @@ -1,30 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model.net; - -import lombok.Getter; -import lombok.Value; - -@Value(staticConstructor = "of") -public class OptableError { - - String message; - - Type type; - - @Getter - public enum Type { - - BAD_INPUT(2), - - BAD_SERVER_RESPONSE(3), - - TIMEOUT(4), - - GENERIC(5); - - private final Integer code; - - Type(Integer errorCode) { - this.code = errorCode; - } - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 0ed5b0fa126..f78da9289fc 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -75,17 +75,17 @@ public Future> call(AuctionResponsePayl moduleContext.setAdserverTargetingEnabled(adserverTargeting); moduleContext.setOptableTargetingExecutionTime(extractOptableTargetingExecutionTime(invocationContext)); - if (adserverTargeting) { - final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( - auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); - moduleContext.setEnrichResponseStatus(validationStatus); - - if (validationStatus.getStatus() == Status.SUCCESS) { - return enrichedPayload(moduleContext); - } + if (!adserverTargeting) { + return success(moduleContext); } - return success(moduleContext); + final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( + auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); + moduleContext.setEnrichResponseStatus(validationStatus); + + return validationStatus.getStatus() == Status.SUCCESS + ? enrichedPayload(moduleContext) + : success(moduleContext); } private long extractOptableTargetingExecutionTime(AuctionInvocationContext invocationContext) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index b33cde01c05..c1a651f3882 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -12,7 +12,6 @@ import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; -import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; @@ -33,13 +32,11 @@ import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; import java.util.Objects; -import java.util.Optional; -import java.util.function.Function; public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { public static final String CODE = "optable-targeting-processed-auction-request-hook"; - private static final long DEFAULT_API_CALL_TIMEOUT = 1000L; + private final ConfigResolver configResolver; private final OptableTargeting optableTargeting; private final PayloadResolver payloadResolver; @@ -93,9 +90,6 @@ public Future> call(AuctionRequestPayloa private BidRequest applyActivityRestrictions(BidRequest bidRequest, AuctionInvocationContext auctionInvocationContext) { - if (bidRequest == null) { - return null; - } final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( @@ -103,18 +97,20 @@ private BidRequest applyActivityRestrictions(BidRequest bidRequest, bidRequest); final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); - final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed(Activity.TRANSMIT_UFPD, - activityInvocationPayload); - final boolean disallowTransmitEids = !activityInfrastructure.isAllowed(Activity.TRANSMIT_EIDS, - activityInvocationPayload); - final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed(Activity.TRANSMIT_GEO, - activityInvocationPayload); + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_UFPD, activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_EIDS, activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_GEO, activityInvocationPayload); return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); } - private BidRequest maskUserPersonalInfo(BidRequest bidRequest, boolean disallowTransmitUfpd, - boolean disallowTransmitEids, boolean disallowTransmitGeo) { + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, + boolean disallowTransmitUfpd, + boolean disallowTransmitEids, + boolean disallowTransmitGeo) { final User maskedUser = userFpdActivityMask.maskUser( bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); @@ -128,10 +124,7 @@ private BidRequest maskUserPersonalInfo(BidRequest bidRequest, boolean disallowT } private long getHookRemainingTime(AuctionInvocationContext invocationContext) { - return Optional.ofNullable(invocationContext) - .map(AuctionInvocationContext::timeout) - .map(Timeout::remaining) - .orElse(DEFAULT_API_CALL_TIMEOUT); + return invocationContext.timeout().remaining(); } private Future> enrichedPayload(TargetingResult targetingResult, @@ -141,10 +134,7 @@ private Future> enrichedPayload(Targetin moduleContext.setTargeting(targetingResult.getAudience()); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - return update(payload -> { - final AuctionRequestPayload sanitizedPayload = sanitizePayload(payload); - return enrichPayload(sanitizedPayload, targetingResult); - }, moduleContext); + return update(payload -> enrichPayload(sanitizePayload(payload), targetingResult), moduleContext); } else { moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); return failure(this::sanitizePayload, moduleContext); @@ -160,43 +150,23 @@ private AuctionRequestPayload sanitizePayload(AuctionRequestPayload payload) { } private static Future> update( - PayloadUpdate func, + PayloadUpdate payloadUpdate, ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) - .payloadUpdate(func::apply) + .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) .build()); } - private static Future> failure(ModuleContext moduleContext) { - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return success(moduleContext); - } - private static Future> failure( - Function func, + PayloadUpdate payloadUpdate, ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.update) - .payloadUpdate(func::apply) - .moduleContext(moduleContext) - .build()); - } - - private static Future> success(ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .moduleContext(moduleContext) - .build()); + return update(payloadUpdate, moduleContext); } @Override diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index ab7f4eb31d0..7214f6ce6a3 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -14,6 +14,8 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OS; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; import java.util.Collections; import java.util.HashMap; @@ -24,15 +26,20 @@ public class IdsMapper { + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(IdsMapper.class)); + private static final Map STATIC_PPID_MAPPING = Map.of( "id5-sync.com", Id.ID5, "utiq.com", Id.UTIQ, "netid.de", Id.NET_ID); private final ObjectMapper objectMapper; + private final double logSamplingRate; - public IdsMapper(ObjectMapper objectMapper) { + public IdsMapper(ObjectMapper objectMapper, double logSamplingRate) { this.objectMapper = Objects.requireNonNull(objectMapper); + this.logSamplingRate = logSamplingRate; } public List toIds(BidRequest bidRequest, Map ppidMapping) { @@ -65,8 +72,8 @@ private ExtUserOptable parseExtUserOptable(JsonNode node) { try { return objectMapper.treeToValue(node, ExtUserOptable.class); } catch (JsonProcessingException e) { - // TODO: you need to handle this exception in some way - throw new RuntimeException(e); + conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate); + return null; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index ff63105917f..81f057afcf4 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -3,6 +3,7 @@ import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; @@ -50,6 +51,10 @@ public static List resolveIp(AuctionContext auctionContext) { return result; } + public static String resolveIp(List ips) { + return CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none"; + } + private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) { final Optional gppContextOpt = Optional.ofNullable(auctionContext) .map(AuctionContext::getGppContext); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 14c94d23000..053aa7c22e8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -19,9 +19,15 @@ public class OptableTargeting { private final Cache cache; - private boolean moduleCacheEnabled = false; + private final boolean moduleCacheEnabled; + private final IdsMapper idsMapper; + private final QueryBuilder queryBuilder; + private final APIClient apiClient; - public OptableTargeting(Cache cache, IdsMapper idsMapper, QueryBuilder queryBuilder, APIClient apiClient, + public OptableTargeting(Cache cache, + IdsMapper idsMapper, + QueryBuilder queryBuilder, + APIClient apiClient, boolean moduleCacheEnabled) { this.cache = Objects.requireNonNull(cache); @@ -31,12 +37,10 @@ public OptableTargeting(Cache cache, IdsMapper idsMapper, QueryBuilder queryBuil this.moduleCacheEnabled = moduleCacheEnabled; } - private final IdsMapper idsMapper; - private final QueryBuilder queryBuilder; - private final APIClient apiClient; - - public Future getTargeting(OptableTargetingProperties properties, BidRequest bidRequest, - OptableAttributes attributes, long timeout) { + public Future getTargeting(OptableTargetingProperties properties, + BidRequest bidRequest, + OptableAttributes attributes, + long timeout) { return Optional.ofNullable(bidRequest) .map(it -> idsMapper.toIds(it, properties.getPpidMapping())) @@ -59,9 +63,13 @@ public Future getTargeting(OptableTargetingProperties propertie .orElse(Future.failedFuture("Can't get targeting")); } - private Future getOrFetchTargetingResults(CacheProperties cacheProperties, String apiKey, - String tenant, String origin, - Query query, List ips, long timeout) { + private Future getOrFetchTargetingResults(CacheProperties cacheProperties, + String apiKey, + String tenant, + String origin, + Query query, + List ips, + long timeout) { final String cachingKey = createCachingKey(tenant, origin, ips, query, true); return cache.get(cachingKey) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 77cc4438f30..e34f97ed5e6 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -63,6 +63,7 @@ public void setUp() { when(timeout.remaining()).thenReturn(1000L); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); + when(invocationContext.timeout()).thenReturn(timeout); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); payloadResolver = new PayloadResolver(mapper); optableAttributesResolver = new OptableAttributesResolver(); @@ -108,7 +109,7 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin @Test public void shouldReturnResultWithCleanedUpUserExtOptableTag() { // given - when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(false)); + when(invocationContext.timeout()).thenReturn(timeout); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java index 5f63f91afb8..a72d955a055 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java @@ -30,7 +30,7 @@ public class IdMapperTest { @BeforeEach public void setUp() { - target = new IdsMapper(objectMapper); + target = new IdsMapper(objectMapper, 0.01); } @Test From 5857d7f38f2594ae7d8edecaca914d0ccb01eec8 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Tue, 10 Jun 2025 00:10:05 +0200 Subject: [PATCH 25/41] optable-targeting: Never return Future as a null --- .../targeting/model/net/OptableCall.java | 4 ++ ...eTargetingProcessedAuctionRequestHook.java | 25 +++-------- .../optable/targeting/v1/net/APIClient.java | 42 +++++++++++-------- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java index bd3e98a3470..f80d6db1ab2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java @@ -19,4 +19,8 @@ public static OptableCall succeededHttp(HttpRequest request, HttpResponse respon public static OptableCall failedHttp(HttpRequest request) { return new OptableCall(request, null); } + + public static OptableCall failedHttp(HttpResponse response) { + return new OptableCall(null, response); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index c1a651f3882..75549542208 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -70,18 +70,8 @@ public Future> call(AuctionRequestPayloa invocationContext.auctionContext(), properties.getTimeout()); - final Future targetingResultFuture = optableTargeting.getTargeting( - properties, - bidRequest, - attributes, - timeout); - - if (targetingResultFuture == null) { - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return failure(this::sanitizePayload, moduleContext); - } - - return targetingResultFuture.compose(targetingResult -> enrichedPayload(targetingResult, moduleContext)) + return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) + .compose(targetingResult -> enrichedPayload(targetingResult, moduleContext)) .recover(throwable -> { moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); return failure(this::sanitizePayload, moduleContext); @@ -130,15 +120,10 @@ private long getHookRemainingTime(AuctionInvocationContext invocationContext) { private Future> enrichedPayload(TargetingResult targetingResult, ModuleContext moduleContext) { - if (targetingResult != null) { - moduleContext.setTargeting(targetingResult.getAudience()); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - return update(payload -> enrichPayload(sanitizePayload(payload), targetingResult), moduleContext); - } else { - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return failure(this::sanitizePayload, moduleContext); - } + return update(payload -> enrichPayload(sanitizePayload(payload), targetingResult), moduleContext); } private AuctionRequestPayload enrichPayload(AuctionRequestPayload payload, TargetingResult targetingResult) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index de6da70a226..99c6951f6e9 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -79,27 +79,31 @@ private Future doRequest(HttpRequest httpRequest, long timeout) return createRequest(httpRequest, timeout) .compose(response -> processResponse(response, httpRequest)) .recover(exception -> failResponse(exception, httpRequest)) - .map(this::validateResponse) - .map(it -> parseSilent(it, httpRequest)) + .compose(this::validateResponse) + .compose(it -> parseSilent(it, httpRequest)) .recover(exception -> logParsingError(exception, httpRequest)); } - private TargetingResult parseSilent(OptableCall optableCall, HttpRequest httpRequest) { + private Future parseSilent(OptableCall optableCall, HttpRequest httpRequest) { try { - return responseMapper.parse(optableCall); + final TargetingResult result = responseMapper.parse(optableCall); + return result != null + ? Future.succeededFuture(result) + : Future.failedFuture("Can't parse Optable response"); } catch (Exception e) { logParsingError(e, httpRequest); } - return null; + return Future.failedFuture("Can't parse Optable response"); } private Future logParsingError(Throwable exception, HttpRequest httpRequest) { - logger.warn("Error occurred while parsing HTTP response from the Optable url: %s with message: %s" - .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); + final String error = "Error occurred while parsing HTTP response from the Optable url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()); + logger.warn(error, logSamplingRate); logger.debug("Error occurred while parsing HTTP response from the Optable url: {}", exception, httpRequest.getUri()); - return null; + return Future.failedFuture(error); } private Future createRequest(HttpRequest httpRequest, long remainingTimeout) { @@ -123,29 +127,31 @@ private static Future processResponse(HttpClientResponse response, } private Future failResponse(Throwable exception, HttpRequest httpRequest) { - conditionalLogger.warn("Error occurred while sending HTTP request to the Optable url: %s with message: %s" - .formatted(httpRequest.getUri(), exception.getMessage()), logSamplingRate); + final String error = "Error occurred while sending HTTP request to the Optable url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()); + conditionalLogger.warn(error, logSamplingRate); logger.debug("Error occurred while sending HTTP request to the Optable url: {}", exception, httpRequest.getUri()); - return Future.succeededFuture(OptableCall.failedHttp(httpRequest)); + return Future.failedFuture(error); } - private OptableCall validateResponse(OptableCall response) { + private Future validateResponse(OptableCall response) { return Optional.ofNullable(response) .map(OptableCall::getResponse) .map(resp -> { if (resp.getStatusCode() != HttpResponseStatus.OK.code()) { - conditionalLogger.warn(("Error occurred while sending HTTP request to the " - + "Optable url: %s with message: %s").formatted(response.getRequest().getUri(), - resp.getBody()), logSamplingRate); + final String error = "Error occurred while sending HTTP request to the " + + "Optable url: %s with message: %s".formatted(response.getRequest().getUri(), + resp.getBody()); + conditionalLogger.warn(error, logSamplingRate); logger.debug("Error occurred while sending HTTP request to the Optable url: {}"); - return null; + return Future.failedFuture(error); } - return response; + return Future.succeededFuture(response); }) - .orElse(null); + .orElse(Future.failedFuture("OptableCall validation error")); } } From e50bb0d43790a9c36451ee1c858f8e9f0634acd1 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Tue, 10 Jun 2025 20:23:38 +0200 Subject: [PATCH 26/41] optable-targeting: update caching integration --- .../modules/optable/targeting/v1/core/Cache.java | 3 +-- .../targeting/v1/core/OptableTargeting.java | 14 ++++++++------ .../targeting/v1/core/OptableTargetingTest.java | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index a0973acc7de..9e03fdbe24f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -27,8 +27,7 @@ public Cache(PbcStorageService cacheService, public Future get(String query) { return cacheService.retrieveEntry(query, APP_CODE, APPLICATION) .map(ModuleCacheResponse::getValue) - .map(it -> it != null ? optableResponseMapper.parse(it) : null) - .otherwiseEmpty(); + .map(optableResponseMapper::parse); } public Future put(String query, TargetingResult value, int ttlSeconds) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 053aa7c22e8..3332c0fc719 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -73,12 +73,14 @@ private Future getOrFetchTargetingResults(CacheProperties cache final String cachingKey = createCachingKey(tenant, origin, ips, query, true); return cache.get(cachingKey) - .recover(err -> Future.succeededFuture(null)) - .compose(entry -> entry != null - ? Future.succeededFuture(entry) - : fetchAndCacheResult(tenant, origin, cacheProperties.getTtlseconds(), apiKey, - query, ips, timeout)); - + .recover(ignore -> fetchAndCacheResult( + tenant, + origin, + cacheProperties.getTtlseconds(), + apiKey, + query, + ips, + timeout)); } private Future fetchAndCacheResult(String tenant, String origin, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 66475a5aac1..08d86e5e3e3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -85,7 +85,7 @@ public void shouldReturnTargetingResultsAndNotUseCache() { @Test public void shouldCallAPIAndAddTargetingResultsToCache() { // given - when(cache.get(any())).thenReturn(Future.succeededFuture(null)); + when(cache.get(any())).thenReturn(Future.failedFuture("error")); when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final BidRequest bidRequest = givenBidRequest(); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); From c450374ae0cdcb19a7c0a2f8bf0f0584e371c919 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Wed, 11 Jun 2025 18:39:48 +0200 Subject: [PATCH 27/41] optable-targeting: Code cleanup. Replaced payload resolvers by payload updaters --- .../config/OptableTargetingConfig.java | 9 - .../OptableTargetingAuctionResponseHook.java | 13 +- ...eTargetingProcessedAuctionRequestHook.java | 27 +-- .../targeting/v1/core/BidRequestCleaner.java | 55 ++++++ .../targeting/v1/core/BidRequestEnricher.java | 83 ++++++++++ ...Resolver.java => BidResponseEnricher.java} | 48 ++++-- .../targeting/v1/core/PayloadResolver.java | 62 ------- .../v1/core/merger/BidRequestBuilder.java | 98 ----------- .../targeting/v1/core/merger/ExtMerger.java | 2 +- .../optable/targeting/v1/BaseOptableTest.java | 5 + ...tableTargetingAuctionResponseHookTest.java | 5 - .../v1/OptableTargetingModuleTest.java | 6 - ...getingProcessedAuctionRequestHookTest.java | 10 +- .../v1/core/BidRequestCleanerTest.java | 30 ++++ .../v1/core/BidRequestEnricherTest.java | 76 +++++++++ .../v1/core/BidResponseEnricherTest.java | 80 +++++++++ .../v1/core/PayloadResolverTest.java | 156 ------------------ 17 files changed, 377 insertions(+), 388 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java rename extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/{merger/BidResponseResolver.java => BidResponseEnricher.java} (54%) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 1e858c60645..f8fe17a2a2e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -12,7 +12,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; @@ -47,11 +46,6 @@ IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") doubl return new IdsMapper(ObjectMapperProvider.mapper(), logSamplingRate); } - @Bean - PayloadResolver payloadResolver() { - return new PayloadResolver(ObjectMapperProvider.mapper()); - } - @Bean QueryBuilder queryBuilder() { return new QueryBuilder(); @@ -120,7 +114,6 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, - PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver, UserFpdActivityMask userFpdActivityMask) { @@ -128,11 +121,9 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - payloadResolver, optableAttributesResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( - payloadResolver, configResolver, ObjectMapperProvider.mapper()))); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index f78da9289fc..fd32724b78a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -14,7 +14,6 @@ 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.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Reason; @@ -22,8 +21,8 @@ import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -49,17 +48,13 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; private static final String STATUS_EXECUTION_TIME = "execution-time"; private static final String STATUS_REASON = "reason"; - - private final PayloadResolver payloadResolver; private final ConfigResolver configResolver; private final ObjectMapper objectMapper; public OptableTargetingAuctionResponseHook( - PayloadResolver payloadResolver, ConfigResolver configResolver, ObjectMapper objectMapper) { - this.payloadResolver = Objects.requireNonNull(payloadResolver); this.configResolver = Objects.requireNonNull(configResolver); this.objectMapper = Objects.requireNonNull(objectMapper); } @@ -112,14 +107,10 @@ private Future> enrichedPayload(ModuleC final List targeting = moduleContext.getTargeting(); return CollectionUtils.isNotEmpty(targeting) - ? update(payload -> enrichPayload(payload, targeting), moduleContext) + ? update(BidResponseEnricher.of(objectMapper, targeting), moduleContext) : success(moduleContext); } - private AuctionResponsePayload enrichPayload(AuctionResponsePayload payload, List targeting) { - return AuctionResponsePayloadImpl.of(payloadResolver.enrichBidResponse(payload.bidResponse(), targeting)); - } - private Future> update( PayloadUpdate payloadUpdate, ModuleContext moduleContext) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 75549542208..86cfc978753 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -13,16 +13,16 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; -import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -32,6 +32,7 @@ import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; import java.util.Objects; +import java.util.function.Function; public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { @@ -39,19 +40,16 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final ConfigResolver configResolver; private final OptableTargeting optableTargeting; - private final PayloadResolver payloadResolver; private final OptableAttributesResolver optableAttributesResolver; private final UserFpdActivityMask userFpdActivityMask; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, OptableTargeting optableTargeting, - PayloadResolver payloadResolver, OptableAttributesResolver optableAttributesResolver, UserFpdActivityMask userFpdActivityMask) { this.configResolver = Objects.requireNonNull(configResolver); this.optableTargeting = Objects.requireNonNull(optableTargeting); - this.payloadResolver = Objects.requireNonNull(payloadResolver); this.optableAttributesResolver = Objects.requireNonNull(optableAttributesResolver); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } @@ -74,7 +72,7 @@ public Future> call(AuctionRequestPayloa .compose(targetingResult -> enrichedPayload(targetingResult, moduleContext)) .recover(throwable -> { moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return failure(this::sanitizePayload, moduleContext); + return failure(BidRequestCleaner.instance(), moduleContext); }); } @@ -122,27 +120,20 @@ private Future> enrichedPayload(Targetin moduleContext.setTargeting(targetingResult.getAudience()); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - - return update(payload -> enrichPayload(sanitizePayload(payload), targetingResult), moduleContext); - } - - private AuctionRequestPayload enrichPayload(AuctionRequestPayload payload, TargetingResult targetingResult) { - return AuctionRequestPayloadImpl.of(payloadResolver.enrichBidRequest(payload.bidRequest(), targetingResult)); - } - - private AuctionRequestPayload sanitizePayload(AuctionRequestPayload payload) { - return AuctionRequestPayloadImpl.of(payloadResolver.clearBidRequest(payload.bidRequest())); + return update( + BidRequestEnricher.of(targetingResult).compose(BidRequestCleaner.instance()), + moduleContext); } private static Future> update( - PayloadUpdate payloadUpdate, + Function payloadUpdate, ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) - .payloadUpdate(payloadUpdate) + .payloadUpdate(payloadUpdate::apply) .moduleContext(moduleContext) .build()); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java new file mode 100644 index 00000000000..e25ba02b0c5 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java @@ -0,0 +1,55 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import lombok.Value; +import lombok.experimental.Accessors; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.PayloadCleaner; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; + +import java.util.Optional; + +@Accessors(fluent = true) +@Value(staticConstructor = "instance") +public class BidRequestCleaner implements PayloadUpdate { + + @Override + public AuctionRequestPayload apply(AuctionRequestPayload payload) { + return AuctionRequestPayloadImpl.of(clearExtUserOptable(payload.bidRequest())); + } + + private static BidRequest clearExtUserOptable(BidRequest bidRequest) { + final Optional userOpt = getUserOpt(bidRequest); + + final JsonNode userExtOptable = userOpt.map(User::getExt) + .map(it -> it.getProperty("optable")) + .map(it -> PayloadCleaner.cleanUserExtOptable((ObjectNode) it)) + .orElse(null); + + if (userExtOptable != null) { + final ExtUser extUser = userOpt.map(User::getExt).orElse(null); + if (!userExtOptable.isEmpty()) { + extUser.addProperty("optable", userExtOptable); + } else { + extUser.addProperty("optable", null); + } + return bidRequest.toBuilder() + .user(userOpt.get().toBuilder() + .ext(extUser) + .build()) + .build(); + } + + return bidRequest; + } + + private static Optional getUserOpt(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getUser); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java new file mode 100644 index 00000000000..9d206b508fa --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -0,0 +1,83 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; +import lombok.Value; +import lombok.experimental.Accessors; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.DataMerger; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.EidsMerger; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidRequestEnricher implements PayloadUpdate { + + TargetingResult targetingResult; + + @Override + public AuctionRequestPayload apply(AuctionRequestPayload payload) { + return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest(), targetingResult)); + } + + private static BidRequest enrichBidRequest(BidRequest bidRequest, TargetingResult targetingResults) { + if (bidRequest == null || targetingResults == null) { + return bidRequest; + } + + final User optableUser = getUser(targetingResults); + if (optableUser == null) { + return bidRequest; + } + + final com.iab.openrtb.request.User bidRequestUser = getOrCreateUser(bidRequest); + + return bidRequest.toBuilder() + .user(mergeUserData(bidRequestUser, optableUser)) + .build(); + } + + private static com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { + final com.iab.openrtb.request.User.UserBuilder userBuilder = user.toBuilder(); + + final List eids = optableUser.getEids(); + final List data = optableUser.getData(); + + if (!CollectionUtils.isEmpty(eids)) { + userBuilder.eids(EidsMerger.merge(user.getEids(), eids)); + } + + if (!CollectionUtils.isEmpty(data)) { + userBuilder.data(DataMerger.merge(user.getData(), data)); + } + + return userBuilder.build(); + } + + private static User getUser(TargetingResult targetingResults) { + return Optional.ofNullable(targetingResults) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .orElse(null); + } + + private static com.iab.openrtb.request.User getOrCreateUser(BidRequest bidRequest) { + return getUserOpt(bidRequest) + .orElseGet(() -> com.iab.openrtb.request.User.builder().eids(new ArrayList<>()).build()); + } + + private static Optional getUserOpt(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(BidRequest::getUser); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java similarity index 54% rename from extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java rename to extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java index 04ad62f739f..48cf2f6ebbf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidResponseResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java @@ -1,4 +1,4 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; +package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; @@ -7,27 +7,41 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import lombok.AllArgsConstructor; +import lombok.Value; +import lombok.experimental.Accessors; import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.ExtMerger; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; import java.util.Collections; import java.util.List; import java.util.Optional; -@AllArgsConstructor(staticName = "of") -public class BidResponseResolver { +@Accessors(fluent = true) +@Value(staticConstructor = "of") +public class BidResponseEnricher implements PayloadUpdate { - private BidResponse bidResponse; - private ObjectMapper mapper; + ObjectMapper mapper; + List targeting; - public BidResponse applyTargeting(List targeting) { - if (CollectionUtils.isEmpty(targeting)) { + @Override + public AuctionResponsePayload apply(AuctionResponsePayload payload) { + return AuctionResponsePayloadImpl.of(enrichBidResponse(mapper, payload.bidResponse(), targeting)); + } + + private static BidResponse enrichBidResponse(ObjectMapper mapper, + BidResponse bidResponse, + List targeting) { + + if (bidResponse == null || CollectionUtils.isEmpty(targeting)) { return bidResponse; } - final ObjectNode node = targetingToObjectNode(targeting); + final ObjectNode node = targetingToObjectNode(mapper, targeting); if (node == null) { return bidResponse; } @@ -38,7 +52,7 @@ public BidResponse applyTargeting(List targeting) { final List bids = Optional.ofNullable(seatBid.getBid()) .orElse(Collections.emptyList()) .stream() - .map(bid -> applyTargeting(bid, node)) + .map(bid -> applyTargeting(mapper, bid, node)) .toList(); return seatBid.toBuilder().bid(bids).build(); @@ -49,10 +63,10 @@ public BidResponse applyTargeting(List targeting) { .build(); } - private Bid applyTargeting(Bid bid, ObjectNode node) { - final ObjectNode extNode = getOrCreateNode(bid.getExt()); - final ObjectNode prebidNode = getOrCreateNode((ObjectNode) extNode.get("prebid")); - final ObjectNode targetingNode = getOrCreateNode((ObjectNode) prebidNode.get("targeting")); + private static Bid applyTargeting(ObjectMapper mapper, Bid bid, ObjectNode node) { + final ObjectNode extNode = getOrCreateNode(mapper, bid.getExt()); + final ObjectNode prebidNode = getOrCreateNode(mapper, (ObjectNode) extNode.get("prebid")); + final ObjectNode targetingNode = getOrCreateNode(mapper, (ObjectNode) prebidNode.get("targeting")); final JsonNode mergedTargetingNode = ExtMerger.mergeExt(targetingNode, node); prebidNode.set("targeting", mergedTargetingNode); @@ -61,11 +75,11 @@ private Bid applyTargeting(Bid bid, ObjectNode node) { return bid.toBuilder().ext(extNode).build(); } - private ObjectNode getOrCreateNode(ObjectNode node) { - return Optional.ofNullable(node).orElseGet(() -> mapper.createObjectNode()); + private static ObjectNode getOrCreateNode(ObjectMapper mapper, ObjectNode node) { + return Optional.ofNullable(node).orElseGet(mapper::createObjectNode); } - private ObjectNode targetingToObjectNode(List targeting) { + private static ObjectNode targetingToObjectNode(ObjectMapper mapper, List targeting) { final ObjectNode node = mapper.createObjectNode(); for (Audience audience: targeting) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java deleted file mode 100644 index 1eb734ab25e..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolver.java +++ /dev/null @@ -1,62 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.response.BidResponse; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidRequestBuilder; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.BidResponseResolver; - -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class PayloadResolver { - - private ObjectMapper mapper; - - public PayloadResolver(ObjectMapper mapper) { - this.mapper = Objects.requireNonNull(mapper); - } - - public BidRequest enrichBidRequest(BidRequest bidRequest, TargetingResult targetingResults) { - if (bidRequest == null || targetingResults == null) { - return bidRequest; - } - - final User user = getUser(targetingResults); - if (user == null) { - return bidRequest; - } - - return new BidRequestBuilder(bidRequest) - .addEids(user.getEids()) - .addData(user.getData()) - .build(); - } - - public BidResponse enrichBidResponse(BidResponse bidResponse, List targeting) { - if (bidResponse == null || CollectionUtils.isEmpty(targeting)) { - return bidResponse; - } - - return BidResponseResolver.of(bidResponse, mapper).applyTargeting(targeting); - } - - public BidRequest clearBidRequest(BidRequest bidRequest) { - return bidRequest != null - ? new BidRequestBuilder(bidRequest).clearExtUserOptable().build() - : null; - } - - private static User getUser(TargetingResult targetingResults) { - return Optional.ofNullable(targetingResults) - .map(TargetingResult::getOrtb2) - .map(Ortb2::getUser) - .orElse(null); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java deleted file mode 100644 index cb9a9520a65..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BidRequestBuilder.java +++ /dev/null @@ -1,98 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.User; -import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; -import org.prebid.server.proto.openrtb.ext.request.ExtUser; - -import java.util.ArrayList; -import java.util.List; -import java.util.Objects; -import java.util.Optional; - -public class BidRequestBuilder { - - private BidRequest bidRequest; - - public BidRequestBuilder(BidRequest bidRequest) { - this.bidRequest = Objects.requireNonNull(bidRequest); - } - - public BidRequestBuilder addEids(List eids) { - if (bidRequest == null || CollectionUtils.isEmpty(eids)) { - return this; - } - - final User user = getOrCreateUser(bidRequest); - bidRequest = bidRequest.toBuilder() - .user(user.toBuilder() - .eids(EidsMerger.merge(user.getEids(), eids)) - .build()) - .build(); - - return this; - } - - public BidRequestBuilder addData(List data) { - if (bidRequest == null || CollectionUtils.isEmpty(data)) { - return this; - } - - final User user = getOrCreateUser(bidRequest); - - bidRequest = bidRequest.toBuilder() - .user(user.toBuilder() - .data(DataMerger.merge(user.getData(), data)) - .build()) - .build(); - - return this; - } - - private static Optional getUserOpt(BidRequest bidRequest) { - return Optional.ofNullable(bidRequest) - .map(BidRequest::getUser); - } - - private static User getOrCreateUser(BidRequest bidRequest) { - return getUserOpt(bidRequest) - .orElseGet(() -> User.builder().eids(new ArrayList<>()).build()); - } - - public BidRequest build() { - return bidRequest; - } - - public BidRequestBuilder clearExtUserOptable() { - if (bidRequest == null) { - return this; - } - - final Optional userOpt = getUserOpt(bidRequest); - - final JsonNode userExtOptable = userOpt.map(User::getExt) - .map(it -> it.getProperty("optable")) - .map(it -> PayloadCleaner.cleanUserExtOptable((ObjectNode) it)) - .orElse(null); - - if (userExtOptable != null) { - final ExtUser extUser = userOpt.map(User::getExt).orElse(null); - if (!userExtOptable.isEmpty()) { - extUser.addProperty("optable", userExtOptable); - } else { - extUser.addProperty("optable", null); - } - bidRequest = bidRequest.toBuilder() - .user(userOpt.get().toBuilder() - .ext(extUser) - .build()) - .build(); - } - - return this; - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java index 09d1cfd01bd..a8977d45739 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java @@ -9,7 +9,7 @@ public class ExtMerger { private ExtMerger() { } - protected static ObjectNode mergeExt(ObjectNode origin, ObjectNode newExt) { + public static ObjectNode mergeExt(ObjectNode origin, ObjectNode newExt) { if (newExt == null) { return origin; } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 12a471027b9..fa60460e073 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -43,6 +43,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.function.UnaryOperator; public abstract class BaseOptableTest { @@ -76,6 +77,10 @@ protected BidRequest givenBidRequest() { .build(); } + protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build(); + } + protected BidResponse givenBidResponse() { final ObjectNode targetingNode = mapper.createObjectNode(); targetingNode.set("attribute1", TextNode.valueOf("value1")); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 3c8f1913d5a..b3c9b0b5cf6 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -14,7 +14,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -37,7 +36,6 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { AuctionResponsePayload auctionResponsePayload; @Mock(strictness = LENIENT) AuctionInvocationContext invocationContext; - private PayloadResolver payloadResolver; private AuctionResponseValidator auctionResponseValidator; private final ObjectMapper mapper = new ObjectMapper(); private final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); @@ -48,10 +46,8 @@ public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { @BeforeEach public void setUp() { when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); - payloadResolver = new PayloadResolver(mapper); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); target = new OptableTargetingAuctionResponseHook( - payloadResolver, configResolver, mapper); } @@ -128,7 +124,6 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", List.of(new AudienceId("audienceId")), "keyspace", 1)))); target = new OptableTargetingAuctionResponseHook( - payloadResolver, configResolver, mapper); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 3e3839ab704..c101f6c0c25 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -9,7 +9,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; import org.prebid.server.hooks.v1.Module; @@ -33,9 +32,6 @@ public class OptableTargetingModuleTest { @Mock OptableTargeting optableTargeting; - @Mock - PayloadResolver payloadResolver; - @Mock OptableAttributesResolver optableAttributesResolver; @@ -65,11 +61,9 @@ public void shouldReturnHooks() { List.of(new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - payloadResolver, optableAttributesResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( - payloadResolver, configResolver, mapper)); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index e34f97ed5e6..70e4db34d24 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -18,7 +18,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.PayloadResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -50,7 +49,6 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock ActivityInfrastructure activityInfrastructure; private ConfigResolver configResolver; - private PayloadResolver payloadResolver; private OptableAttributesResolver optableAttributesResolver; private JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(new ObjectMapper())); private OptableTargetingProcessedAuctionRequestHook target; @@ -65,10 +63,12 @@ public void setUp() { when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); when(invocationContext.timeout()).thenReturn(timeout); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - payloadResolver = new PayloadResolver(mapper); optableAttributesResolver = new OptableAttributesResolver(); - target = new OptableTargetingProcessedAuctionRequestHook(configResolver, optableTargeting, - payloadResolver, optableAttributesResolver, userFpdActivityMask); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, + optableTargeting, + optableAttributesResolver, + userFpdActivityMask); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java new file mode 100644 index 00000000000..b6d72748fbd --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java @@ -0,0 +1,30 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BidRequestCleanerTest extends BaseOptableTest { + + @Test + public void shouldRemoveUserExtOptableTag() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> + bidRequest.user(givenUser()))); + + // when + final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload); + + // then + assertThat(result).extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getExt) + .extracting(it -> it.getProperty("optable")) + .isEqualTo(null); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java new file mode 100644 index 00000000000..af6b4cb38c0 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.User; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BidRequestEnricherTest extends BaseOptableTest { + + @Test + public void shouldReturnOriginBidRequestWhenNoTargetingResults() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(null).apply(auctionRequestPayload); + + // then + assertThat(result).isNotNull(); + final User user = result.bidRequest().getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids()).isNull(); + assertThat(user.getData()).isNull(); + } + + @Test + public void shouldNotFailIfBidRequestIsNull() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(null); + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult).apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNull(); + } + + @Test + public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); + final TargetingResult targetingResult = givenTargetingResult(); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult).apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final User user = result.bidRequest().getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); + assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + } + + @Test + public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { + // given + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); + final TargetingResult targetingResult = givenEmptyTargetingResult(); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult).apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final User user = result.bidRequest().getUser(); + assertThat(user).isNotNull(); + assertThat(user.getEids()).isNull(); + assertThat(user.getData()).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java new file mode 100644 index 00000000000..ad274002544 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java @@ -0,0 +1,80 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class BidResponseEnricherTest extends BaseOptableTest { + + @Test + public void shouldEnrichBidResponseByTargetingKeywords() { + // given + final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(givenBidResponse()); + + // when + final AuctionResponsePayload result = BidResponseEnricher.of( + mapper, givenTargeting()).apply(auctionResponsePayload); + final ObjectNode targeting = (ObjectNode) result.bidResponse().getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNotNull(); + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId,audienceId2"); + } + + @Test + public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { + // given + final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(givenBidResponse()); + + // when + final AuctionResponsePayload result = BidResponseEnricher.of(mapper, null).apply(auctionResponsePayload); + final ObjectNode targeting = (ObjectNode) result.bidResponse().getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(result).isNotNull(); + assertThat(targeting.get("keyspace")).isNull(); + } + + @Test + public void shouldNotFailWhenResponseIsNull() { + // given + final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(null); + + // when + final AuctionResponsePayload result = BidResponseEnricher.of(mapper, givenTargeting()) + .apply(auctionResponsePayload); + + // then + assertThat(result.bidResponse()).isNull(); + } + + private List givenTargeting() { + return List.of( + new Audience("provider", + List.of( + new AudienceId("audienceId"), + new AudienceId("audienceId2")), + "keyspace", + 1)); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java deleted file mode 100644 index fe9f1ba44b1..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PayloadResolverTest.java +++ /dev/null @@ -1,156 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.User; -import com.iab.openrtb.response.BidResponse; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; - -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; - -public class PayloadResolverTest extends BaseOptableTest { - - private ObjectMapper mapper = new ObjectMapper(); - - private PayloadResolver target; - - @BeforeEach - public void setUp() { - target = new PayloadResolver(mapper); - } - - @Test - public void shouldReturnOriginBidRequestWhenNoTargetingResults() { - // given - final BidRequest bidRequest = givenBidRequest(); - - // when - final BidRequest result = target.enrichBidRequest(bidRequest, null); - - // then - assertThat(result).isNotNull(); - final User user = result.getUser(); - assertThat(user).isNotNull(); - assertThat(user.getEids()).isNull(); - assertThat(user.getData()).isNull(); - } - - @Test - public void shouldNotFailIfBidRequestIsNull() { - // given - final BidRequest bidRequest = null; - final TargetingResult targetingResult = givenTargetingResult(); - - // when - final BidRequest result = target.enrichBidRequest(bidRequest, targetingResult); - - // then - assertThat(result).isNull(); - } - - @Test - public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { - // given - final BidRequest bidRequest = givenBidRequest(); - final TargetingResult targetingResult = givenTargetingResult(); - - // when - final BidRequest result = target.enrichBidRequest(bidRequest, targetingResult); - - // then - assertThat(result).isNotNull(); - final User user = result.getUser(); - assertThat(user).isNotNull(); - assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); - assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); - } - - @Test - public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { - // given - final BidRequest bidRequest = givenBidRequest(); - final TargetingResult targetingResult = givenEmptyTargetingResult(); - - // when - final BidRequest result = target.enrichBidRequest(bidRequest, targetingResult); - - // then - assertThat(result).isNotNull(); - final User user = result.getUser(); - assertThat(user).isNotNull(); - assertThat(user.getEids()).isNull(); - assertThat(user.getData()).isNull(); - } - - @Test - public void shouldEnrichBidResponseByTargetingKeywords() { - // given - final BidResponse bidResponse = givenBidResponse(); - - // when - final BidResponse result = target.enrichBidResponse(bidResponse, givenTargeting()); - final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() - .getFirst() - .getBid() - .getFirst() - .getExt() - .get("prebid") - .get("targeting"); - - // then - assertThat(result).isNotNull(); - assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId,audienceId2"); - } - - @Test - public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { - // given - final BidResponse bidResponse = givenBidResponse(); - - // when - final BidResponse result = target.enrichBidResponse(bidResponse, null); - final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() - .getFirst() - .getBid() - .getFirst() - .getExt() - .get("prebid") - .get("targeting"); - - // then - assertThat(result).isNotNull(); - assertThat(targeting.get("keyspace")).isNull(); - } - - @Test - public void shouldNotFailWhenResponseIsNull() { - // given - final BidResponse bidResponse = givenBidResponse(); - - // when - final BidResponse result = target.enrichBidResponse(null, givenTargeting()); - final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() - .getFirst() - .getBid() - .getFirst() - .getExt() - .get("prebid") - .get("targeting"); - - // then - assertThat(result).isNull(); - } - - private List givenTargeting() { - return List.of(new Audience("provider", - List.of(new AudienceId("audienceId"), new AudienceId("audienceId2")), "keyspace", 1)); - } -} From ec196cb1b7f135249befd7b500a9604692e9f896 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 12 Jun 2025 09:29:54 +0200 Subject: [PATCH 28/41] optable-targeting: Code cleanup. Introduce cached API client --- .../config/OptableTargetingConfig.java | 43 +++-- .../targeting/v1/core/OptableTargeting.java | 78 +-------- .../optable/targeting/v1/net/APIClient.java | 153 +---------------- .../targeting/v1/net/APIClientFactory.java | 21 +++ .../targeting/v1/net/APIClientImpl.java | 159 ++++++++++++++++++ .../targeting/v1/net/CachedAPIClient.java | 71 ++++++++ .../optable/targeting/v1/BaseOptableTest.java | 8 +- .../v1/core/OptableTargetingTest.java | 148 +++------------- .../targeting/v1/net/APIClientTest.java | 32 ++-- .../targeting/v1/net/CachedAPIClientTest.java | 147 ++++++++++++++++ 10 files changed, 485 insertions(+), 375 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index f8fe17a2a2e..893ee91b5aa 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -14,6 +14,9 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientFactory; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; import org.prebid.server.json.JacksonMapper; @@ -21,6 +24,7 @@ import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.spring.config.VertxContextScope; import org.prebid.server.spring.config.model.HttpClientProperties; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -64,20 +68,42 @@ OptableHttpClientWrapper optableHttpClient(Vertx vertx, HttpClientProperties htt return new OptableHttpClientWrapper(vertx, httpClientProperties); } - @Bean - APIClient apiClient(OptableHttpClientWrapper httpClientWrapper, + @Bean(name = "apiClient") + APIClient apiClientImpl(OptableHttpClientWrapper httpClientWrapper, @Value("${logging.sampling-rate:0.01}") double logSamplingRate, OptableTargetingProperties properties, OptableResponseMapper responseParser) { - return new APIClient( + return new APIClientImpl( properties.getApiEndpoint(), httpClientWrapper.getHttpClient(), logSamplingRate, responseParser); } + @Bean(name = "cachedAPIClient") + APIClient cachedApiClient(APIClientImpl apiClient, Cache cache) { + return new CachedAPIClient(apiClient, cache); + } + + @Bean + APIClientFactory apiClientFactory( + @Qualifier("apiClient") + APIClient apiClient, + @Qualifier("cachedAPIClient") + APIClient cachedApiClient, + @Value("${storage.pbc.enabled:false}") + boolean storageEnabled, + @Value("${cache.module.enabled:false}") + boolean moduleCacheEnabled) { + + return new APIClientFactory( + apiClient, + cachedApiClient, + storageEnabled && moduleCacheEnabled); + } + @Bean OptableAttributesResolver optableAttributesResolver() { return new OptableAttributesResolver(); @@ -91,19 +117,12 @@ Cache cache(PbcStorageService cacheService, OptableResponseMapper responseMapper @Bean OptableTargeting optableTargeting(IdsMapper parametersExtractor, QueryBuilder queryBuilder, - APIClient apiClient, - Cache cache, - @Value("${storage.pbc.enabled:false}") - boolean storageEnabled, - @Value("${cache.module.enabled:false}") - boolean moduleCacheEnabled) { + APIClientFactory apiClientFactory) { return new OptableTargeting( - cache, parametersExtractor, queryBuilder, - apiClient, - storageEnabled && moduleCacheEnabled); + apiClientFactory); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 3332c0fc719..b6d84f3a67d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -2,39 +2,27 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; -import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; -import org.prebid.server.hooks.modules.optable.targeting.model.Query; -import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientFactory; -import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; -import java.util.List; import java.util.Objects; import java.util.Optional; public class OptableTargeting { - private final Cache cache; - private final boolean moduleCacheEnabled; private final IdsMapper idsMapper; private final QueryBuilder queryBuilder; - private final APIClient apiClient; + private final APIClientFactory apiClientFactory; - public OptableTargeting(Cache cache, - IdsMapper idsMapper, + public OptableTargeting(IdsMapper idsMapper, QueryBuilder queryBuilder, - APIClient apiClient, - boolean moduleCacheEnabled) { + APIClientFactory apiClientFactory) { - this.cache = Objects.requireNonNull(cache); this.idsMapper = Objects.requireNonNull(idsMapper); this.queryBuilder = Objects.requireNonNull(queryBuilder); - this.apiClient = Objects.requireNonNull(apiClient); - this.moduleCacheEnabled = moduleCacheEnabled; + this.apiClientFactory = Objects.requireNonNull(apiClientFactory); } public Future getTargeting(OptableTargetingProperties properties, @@ -45,62 +33,12 @@ public Future getTargeting(OptableTargetingProperties propertie return Optional.ofNullable(bidRequest) .map(it -> idsMapper.toIds(it, properties.getPpidMapping())) .map(ids -> queryBuilder.build(ids, attributes, properties.getIdPrefixOrder())) - .map(query -> properties.getCache().isEnabled() && moduleCacheEnabled - ? getOrFetchTargetingResults( - properties.getCache(), - properties.getApiKey(), - properties.getTenant(), - properties.getOrigin(), - query, - attributes.getIps(), timeout) - : apiClient.getTargeting( - properties.getApiKey(), - properties.getTenant(), - properties.getOrigin(), + .map(query -> + apiClientFactory.getClient(properties.getCache().isEnabled()).getTargeting( + properties, query, attributes.getIps(), timeout)) .orElse(Future.failedFuture("Can't get targeting")); } - - private Future getOrFetchTargetingResults(CacheProperties cacheProperties, - String apiKey, - String tenant, - String origin, - Query query, - List ips, - long timeout) { - - final String cachingKey = createCachingKey(tenant, origin, ips, query, true); - return cache.get(cachingKey) - .recover(ignore -> fetchAndCacheResult( - tenant, - origin, - cacheProperties.getTtlseconds(), - apiKey, - query, - ips, - timeout)); - } - - private Future fetchAndCacheResult(String tenant, String origin, - int ttlSeconds, String apiKey, Query query, List ips, - long timeout) { - - final String cachingKey = createCachingKey(tenant, origin, ips, query, false); - return apiClient.getTargeting(apiKey, tenant, origin, query, ips, timeout) - .compose(result -> cache.put(cachingKey, result, ttlSeconds) - .recover(throwable -> Future.succeededFuture()) - .map(result)); - } - - private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { - return "%s:%s:%s:%s".formatted( - tenant, - origin, - CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", - encodeQuery - ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) - : query.getIds()); - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index 99c6951f6e9..42dccf9208e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -1,157 +1,16 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; -import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.impl.headers.HeadersMultiMap; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Query; -import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; -import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; -import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.Logger; -import org.prebid.server.log.LoggerFactory; -import org.prebid.server.util.HttpUtil; -import org.prebid.server.vertx.httpclient.HttpClient; -import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.List; -import java.util.Objects; -import java.util.Optional; -public class APIClient { +public interface APIClient { - private static final String TENANT = "{TENANT}"; - private static final String ORIGIN = "{ORIGIN}"; - private static final Logger logger = LoggerFactory.getLogger(APIClient.class); - private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); - private final String endpoint; - private final HttpClient httpClient; - private final double logSamplingRate; - private final OptableResponseMapper responseMapper; - - public APIClient(String endpoint, - HttpClient httpClient, - double logSamplingRate, - OptableResponseMapper responseMapper) { - - this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); - this.httpClient = Objects.requireNonNull(httpClient); - this.logSamplingRate = logSamplingRate; - this.responseMapper = Objects.requireNonNull(responseMapper); - } - - public Future getTargeting(String apiKey, String tenant, String origin, - Query query, List ips, long timeout) { - - final MultiMap headers = HeadersMultiMap.headers() - .add(HttpUtil.ACCEPT_HEADER, "application/json"); - - if (StringUtils.isNotEmpty(apiKey)) { - headers.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer %s".formatted(apiKey)); - } - - if (CollectionUtils.isNotEmpty(ips)) { - ips.forEach(ip -> { - headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip); - }); - } - - final HttpRequest request = HttpRequest.builder() - .uri(resolveEndpoint(endpoint, tenant, origin)) - .query(query.toQueryString()) - .headers(headers) - .build(); - - return doRequest(request, timeout); - } - - private String resolveEndpoint(String endpointTemplate, String tenant, String origin) { - return endpointTemplate.replace(TENANT, tenant) - .replace(ORIGIN, origin); - } - - private Future doRequest(HttpRequest httpRequest, long timeout) { - return createRequest(httpRequest, timeout) - .compose(response -> processResponse(response, httpRequest)) - .recover(exception -> failResponse(exception, httpRequest)) - .compose(this::validateResponse) - .compose(it -> parseSilent(it, httpRequest)) - .recover(exception -> logParsingError(exception, httpRequest)); - } - - private Future parseSilent(OptableCall optableCall, HttpRequest httpRequest) { - try { - final TargetingResult result = responseMapper.parse(optableCall); - return result != null - ? Future.succeededFuture(result) - : Future.failedFuture("Can't parse Optable response"); - } catch (Exception e) { - logParsingError(e, httpRequest); - } - return Future.failedFuture("Can't parse Optable response"); - } - - private Future logParsingError(Throwable exception, HttpRequest httpRequest) { - final String error = "Error occurred while parsing HTTP response from the Optable url: %s with message: %s" - .formatted(httpRequest.getUri(), exception.getMessage()); - logger.warn(error, logSamplingRate); - logger.debug("Error occurred while parsing HTTP response from the Optable url: {}", - exception, httpRequest.getUri()); - - return Future.failedFuture(error); - } - - private Future createRequest(HttpRequest httpRequest, long remainingTimeout) { - try { - return httpClient.request(HttpMethod.GET, - httpRequest.getUri() + "&id=" + httpRequest.getQuery(), - httpRequest.getHeaders(), - (String) null, - remainingTimeout); - } catch (Exception e) { - return Future.failedFuture(e); - } - } - - private static Future processResponse(HttpClientResponse response, - HttpRequest httpRequest) { - - final int statusCode = response.getStatusCode(); - final HttpResponse httpResponse = HttpResponse.of(statusCode, response.getHeaders(), response.getBody()); - return Future.succeededFuture(OptableCall.succeededHttp(httpRequest, httpResponse)); - } - - private Future failResponse(Throwable exception, HttpRequest httpRequest) { - final String error = "Error occurred while sending HTTP request to the Optable url: %s with message: %s" - .formatted(httpRequest.getUri(), exception.getMessage()); - conditionalLogger.warn(error, logSamplingRate); - logger.debug("Error occurred while sending HTTP request to the Optable url: {}", - exception, httpRequest.getUri()); - - return Future.failedFuture(error); - } - - private Future validateResponse(OptableCall response) { - return Optional.ofNullable(response) - .map(OptableCall::getResponse) - .map(resp -> { - if (resp.getStatusCode() != HttpResponseStatus.OK.code()) { - final String error = "Error occurred while sending HTTP request to the " - + "Optable url: %s with message: %s".formatted(response.getRequest().getUri(), - resp.getBody()); - conditionalLogger.warn(error, logSamplingRate); - logger.debug("Error occurred while sending HTTP request to the Optable url: {}"); - - return Future.failedFuture(error); - } - - return Future.succeededFuture(response); - }) - .orElse(Future.failedFuture("OptableCall validation error")); - } + Future getTargeting(OptableTargetingProperties properties, + Query query, + List ips, + long timeout); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java new file mode 100644 index 00000000000..28e178323ae --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java @@ -0,0 +1,21 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import java.util.Objects; + +public class APIClientFactory { + + private final APIClient apiClient; + private final APIClient cachedAPIClient; + + private final boolean moduleCacheEnabled; + + public APIClientFactory(APIClient apiClient, APIClient cachedAPIClient, boolean moduleCacheEnabled) { + this.apiClient = Objects.requireNonNull(apiClient); + this.cachedAPIClient = Objects.requireNonNull(cachedAPIClient); + this.moduleCacheEnabled = moduleCacheEnabled; + } + + public APIClient getClient(boolean isCacheEnabled) { + return isCacheEnabled && moduleCacheEnabled ? cachedAPIClient : apiClient; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java new file mode 100644 index 00000000000..2f33aebd181 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java @@ -0,0 +1,159 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; +import io.vertx.core.MultiMap; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.impl.headers.HeadersMultiMap; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; +import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; +import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.Logger; +import org.prebid.server.log.LoggerFactory; +import org.prebid.server.util.HttpUtil; +import org.prebid.server.vertx.httpclient.HttpClient; +import org.prebid.server.vertx.httpclient.model.HttpClientResponse; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class APIClientImpl implements APIClient { + + private static final String TENANT = "{TENANT}"; + private static final String ORIGIN = "{ORIGIN}"; + private static final Logger logger = LoggerFactory.getLogger(APIClientImpl.class); + private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + private final String endpoint; + private final HttpClient httpClient; + private final double logSamplingRate; + private final OptableResponseMapper responseMapper; + + public APIClientImpl(String endpoint, + HttpClient httpClient, + double logSamplingRate, + OptableResponseMapper responseMapper) { + + this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); + this.httpClient = Objects.requireNonNull(httpClient); + this.logSamplingRate = logSamplingRate; + this.responseMapper = Objects.requireNonNull(responseMapper); + } + + public Future getTargeting(OptableTargetingProperties properties, + Query query, List ips, long timeout) { + + final MultiMap headers = HeadersMultiMap.headers() + .add(HttpUtil.ACCEPT_HEADER, "application/json"); + + final String apiKey = properties.getApiKey(); + if (StringUtils.isNotEmpty(apiKey)) { + headers.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer %s".formatted(apiKey)); + } + + if (CollectionUtils.isNotEmpty(ips)) { + ips.forEach(ip -> { + headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip); + }); + } + + final HttpRequest request = HttpRequest.builder() + .uri(resolveEndpoint(endpoint, properties.getTenant(), properties.getOrigin())) + .query(query.toQueryString()) + .headers(headers) + .build(); + + return doRequest(request, timeout); + } + + private String resolveEndpoint(String endpointTemplate, String tenant, String origin) { + return endpointTemplate.replace(TENANT, tenant) + .replace(ORIGIN, origin); + } + + private Future doRequest(HttpRequest httpRequest, long timeout) { + return createRequest(httpRequest, timeout) + .compose(response -> processResponse(response, httpRequest)) + .recover(exception -> failResponse(exception, httpRequest)) + .compose(this::validateResponse) + .compose(it -> parseSilent(it, httpRequest)) + .recover(exception -> logParsingError(exception, httpRequest)); + } + + private Future parseSilent(OptableCall optableCall, HttpRequest httpRequest) { + try { + final TargetingResult result = responseMapper.parse(optableCall); + return result != null + ? Future.succeededFuture(result) + : Future.failedFuture("Can't parse Optable response"); + } catch (Exception e) { + logParsingError(e, httpRequest); + } + return Future.failedFuture("Can't parse Optable response"); + } + + private Future logParsingError(Throwable exception, HttpRequest httpRequest) { + final String error = "Error occurred while parsing HTTP response from the Optable url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()); + logger.warn(error, logSamplingRate); + logger.debug("Error occurred while parsing HTTP response from the Optable url: {}", + exception, httpRequest.getUri()); + + return Future.failedFuture(error); + } + + private Future createRequest(HttpRequest httpRequest, long remainingTimeout) { + try { + return httpClient.request(HttpMethod.GET, + httpRequest.getUri() + "&id=" + httpRequest.getQuery(), + httpRequest.getHeaders(), + (String) null, + remainingTimeout); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + + private static Future processResponse(HttpClientResponse response, + HttpRequest httpRequest) { + + final int statusCode = response.getStatusCode(); + final HttpResponse httpResponse = HttpResponse.of(statusCode, response.getHeaders(), response.getBody()); + return Future.succeededFuture(OptableCall.succeededHttp(httpRequest, httpResponse)); + } + + private Future failResponse(Throwable exception, HttpRequest httpRequest) { + final String error = "Error occurred while sending HTTP request to the Optable url: %s with message: %s" + .formatted(httpRequest.getUri(), exception.getMessage()); + conditionalLogger.warn(error, logSamplingRate); + logger.debug("Error occurred while sending HTTP request to the Optable url: {}", + exception, httpRequest.getUri()); + + return Future.failedFuture(error); + } + + private Future validateResponse(OptableCall response) { + return Optional.ofNullable(response) + .map(OptableCall::getResponse) + .map(resp -> { + if (resp.getStatusCode() != HttpResponseStatus.OK.code()) { + final String error = "Error occurred while sending HTTP request to the " + + "Optable url: %s with message: %s".formatted(response.getRequest().getUri(), + resp.getBody()); + conditionalLogger.warn(error, logSamplingRate); + logger.debug("Error occurred while sending HTTP request to the Optable url: {}"); + + return Future.failedFuture(error); + } + + return Future.succeededFuture(response); + }) + .orElse(Future.failedFuture("OptableCall validation error")); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java new file mode 100644 index 00000000000..4349a3b79bb --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -0,0 +1,71 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; + +public class CachedAPIClient implements APIClient { + + private final APIClient apiClient; + private final Cache cache; + + public CachedAPIClient(APIClient apiClient, Cache cache) { + this.apiClient = Objects.requireNonNull(apiClient); + this.cache = cache; + } + + @Override + public Future getTargeting(OptableTargetingProperties properties, + Query query, + List ips, + long timeout) { + + final String cachingKey = createCachingKey( + properties.getTenant(), + properties.getOrigin(), + ips, + query, + true); + return cache.get(cachingKey) + .recover(ignore -> fetchAndCacheResult( + properties, + properties.getCache().getTtlseconds(), + query, + ips, + timeout)); + } + + private Future fetchAndCacheResult(OptableTargetingProperties properties, + int ttlSeconds, Query query, List ips, + long timeout) { + + final String cachingKey = createCachingKey( + properties.getTenant(), + properties.getOrigin(), + ips, + query, + false); + return apiClient.getTargeting(properties, query, ips, timeout) + .compose(result -> cache.put(cachingKey, result, ttlSeconds) + .recover(throwable -> Future.succeededFuture()) + .map(result)); + } + + private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { + return "%s:%s:%s:%s".formatted( + tenant, + origin, + CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", + encodeQuery + ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) + : query.getIds()); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index fa60460e073..f62ae34a29a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -177,12 +177,18 @@ protected String givenBodyFromFile(String fileName) { } protected OptableTargetingProperties givenOptableTargetingProperties(boolean enableCache) { + return givenOptableTargetingProperties("key", enableCache); + } + + protected OptableTargetingProperties givenOptableTargetingProperties(String key, boolean enableCache) { final CacheProperties cacheProperties = new CacheProperties(); cacheProperties.setEnabled(enableCache); final OptableTargetingProperties optableTargetingProperties = new OptableTargetingProperties(); optableTargetingProperties.setApiEndpoint("endpoint"); - optableTargetingProperties.setApiKey("key"); + optableTargetingProperties.setTenant("accountId"); + optableTargetingProperties.setOrigin("origin"); + optableTargetingProperties.setApiKey(key); optableTargetingProperties.setPpidMapping(Map.of("c", "id")); optableTargetingProperties.setAdserverTargeting(true); optableTargetingProperties.setTimeout(100L); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 08d86e5e3e3..6e159275382 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -11,21 +11,18 @@ import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientFactory; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -38,7 +35,12 @@ public class OptableTargetingTest extends BaseOptableTest { private IdsMapper idsMapper; @Mock - private APIClient apiClient; + private APIClientImpl apiClient; + + @Mock + private CachedAPIClient cachingAPIClient; + + private APIClientFactory apiClientFactory; private QueryBuilder queryBuilder = new QueryBuilder(); @@ -46,50 +48,23 @@ public class OptableTargetingTest extends BaseOptableTest { private OptableAttributes optableAttributes; - private String idPrefixOrder = "c,c1,email"; - private OptableTargetingProperties properties; @BeforeEach public void setUp() { + apiClientFactory = new APIClientFactory(apiClient, cachingAPIClient, true); optableAttributes = givenOptableAttributes(); properties = givenOptableTargetingProperties(true); - target = new OptableTargeting(cache, idsMapper, queryBuilder, apiClient, true); + target = new OptableTargeting(idsMapper, queryBuilder, apiClientFactory); } @Test - public void shouldReturnTargetingResultsAndNotUseCache() { + public void shouldUseNonCachedAPIClient() { // given final BidRequest bidRequest = givenBidRequest(); properties = givenOptableTargetingProperties(false); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) - .thenReturn(Future.succeededFuture(givenTargetingResult())); - - // when - final Future targetingResult = target.getTargeting( - properties, bidRequest, optableAttributes, 100); - - // then - final User user = targetingResult.result().getOrtb2().getUser(); - assertThat(user).isNotNull() - .returns("source", it -> it.getEids().getFirst().getSource()) - .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(cache, times(0)).get(any()); - verify(apiClient, times(1)).getTargeting(any(), any(), any(), any(), any(), anyLong()); - verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); - } - - @Test - public void shouldCallAPIAndAddTargetingResultsToCache() { - // given - when(cache.get(any())).thenReturn(Future.failedFuture("error")); - when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); - final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -97,23 +72,17 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { properties, bidRequest, optableAttributes, 100); // then - final User user = targetingResult.result().getOrtb2().getUser(); - assertThat(user).isNotNull() - .returns("source", it -> it.getEids().getFirst().getSource()) - .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(cache).put(any(), eq(targetingResult.result()), anyInt()); + assertThat(targetingResult.result()).isNotNull(); + verify(apiClient).getTargeting(any(), any(), any(), anyLong()); } @Test - public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() { + public void shouldUseCachedAPIClient() { // given - when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); - when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final BidRequest bidRequest = givenBidRequest(); + properties = givenOptableTargetingProperties(true); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) + when(cachingAPIClient.getTargeting(any(), any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -121,85 +90,8 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() properties, bidRequest, optableAttributes, 100); // then - final User user = targetingResult.result().getOrtb2().getUser(); - assertThat(user).isNotNull() - .returns("source", it -> it.getEids().getFirst().getSource()) - .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(apiClient, times(1)).getTargeting(any(), any(), any(), any(), any(), anyLong()); - verify(cache).put(any(), eq(targetingResult.result()), anyInt()); - } - - @Test - public void shouldUseCachedResult() { - // given - when(cache.get(any())).thenReturn(Future.succeededFuture(givenTargetingResult())); - final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - - // when - final Future targetingResult = target.getTargeting( - properties, bidRequest, optableAttributes, 100); - - // then - final User user = targetingResult.result().getOrtb2().getUser(); - assertThat(user).isNotNull() - .returns("source", it -> it.getEids().getFirst().getSource()) - .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getId()) - .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(cache, times(1)).get(any()); - verify(apiClient, times(0)).getTargeting(any(), any(), any(), any(), any(), anyLong()); - verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); - } - - @Test - public void shouldNotFailWhenNoIdsMapped() { - // given - final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any(), any())).thenReturn(List.of()); - verifyNoInteractions(apiClient); - - // when - final Future targetingResult = target.getTargeting( - properties, bidRequest, optableAttributes, 100); - - // then - assertThat(targetingResult.result()).isNull(); - } - - @Test - public void shouldNotFailWhenApiClientIsFailed() { - // given - properties = givenOptableTargetingProperties(false); - final BidRequest bidRequest = givenBidRequest(); - when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())).thenReturn(null); - - // when - final Future targetingResult = target.getTargeting(properties, bidRequest, - optableAttributes, 100); - - // then - assertThat(targetingResult.result()).isNull(); - } - - @Test - public void shouldNotFailWhenApiClientReturnsFailFuture() { - // given - final BidRequest bidRequest = givenBidRequest(); - properties = givenOptableTargetingProperties(false); - when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), any(), any(), anyLong())) - .thenReturn(Future.failedFuture("File")); - - // when - final Future targetingResult = target.getTargeting(properties, bidRequest, - optableAttributes, 100); - - // then - assertThat(targetingResult.result()).isNull(); + assertThat(targetingResult.result()).isNotNull(); + verify(cachingAPIClient).getTargeting(any(), any(), any(), anyLong()); } private OptableAttributes givenOptableAttributes() { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index 5c6b4b94051..2ebae99a349 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -48,10 +48,7 @@ public class APIClientTest extends BaseOptableTest { @BeforeEach public void setUp() { - target = new APIClient("http://endpoint.optable.com", - httpClient, - LOG_SAMPLING_RATE, - parser); + target = new APIClientImpl("http://endpoint.optable.com", httpClient, LOG_SAMPLING_RATE, parser); } @Test @@ -61,7 +58,7 @@ public void shouldReturnTargetingResult() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), 1000); // then @@ -79,7 +76,7 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), 1000); // then @@ -93,7 +90,7 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), 1000); // then @@ -107,7 +104,7 @@ public void shouldNotFailWhenHttpClientIsCrashed() { .thenThrow(new NullPointerException()); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), 1000); // then @@ -122,7 +119,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), 1000); // then @@ -132,7 +129,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { @Test public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // given - target = new APIClient("http://endpoint.optable.com", + target = new APIClientImpl("http://endpoint.optable.com", httpClient, LOG_SAMPLING_RATE, parser); @@ -142,7 +139,7 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), 1000); // then @@ -162,8 +159,11 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { "plain_text_response.json"))); // when - final Future result = target.getTargeting(null, "accountId", "origin", - givenQuery(), List.of("8.8.8.8"), 1000); + final Future result = target.getTargeting( + givenOptableTargetingProperties(null, false), + givenQuery(), + List.of("8.8.8.8"), + 1000); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -183,9 +183,7 @@ public void shouldPassThroughIpAddresses() { // when final Future result = target.getTargeting( - "key", - "accountId", - "origin", + givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8", "2001:4860:4860::8888"), 1000); @@ -206,7 +204,7 @@ public void shouldNotPassThroughIpAddressWhenNotSpecified() { "plain_text_response.json"))); // when - final Future result = target.getTargeting("key", "accountId", "origin", + final Future result = target.getTargeting(givenOptableTargetingProperties(false), givenQuery(), null, 1000); // then diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java new file mode 100644 index 00000000000..5e2a8cf07e7 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -0,0 +1,147 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.net; + +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +public class CachedAPIClientTest extends BaseOptableTest { + + @Mock(strictness = Mock.Strictness.LENIENT) + private Cache cache; + + @Mock + private APIClientImpl apiClient; + + private APIClient target; + + private OptableTargetingProperties properties; + + @BeforeEach + public void setUp() { + properties = givenOptableTargetingProperties(true); + target = new CachedAPIClient(apiClient, cache); + } + + @Test + public void shouldCallAPIAndAddTargetingResultsToCache() { + // given + when(cache.get(any())).thenReturn(Future.failedFuture("error")); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); + final Query query = givenQuery(); + when(apiClient.getTargeting(any(), any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting( + properties, query, List.of("8.8.8.8"), 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() { + // given + when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); + final Query query = givenQuery(); + when(apiClient.getTargeting(any(), any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future targetingResult = target.getTargeting( + properties, query, List.of("8.8.8.8"), 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), anyLong()); + verify(cache).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldUseCachedResult() { + // given + when(cache.get(any())).thenReturn(Future.succeededFuture(givenTargetingResult())); + final Query query = givenQuery(); + + // when + final Future targetingResult = target.getTargeting( + properties, query, List.of("8.8.8.8"), 100); + + // then + final User user = targetingResult.result().getOrtb2().getUser(); + assertThat(user).isNotNull() + .returns("source", it -> it.getEids().getFirst().getSource()) + .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getId()) + .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); + verify(cache, times(1)).get(any()); + verify(apiClient, times(0)).getTargeting(any(), any(), any(), anyLong()); + verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); + } + + @Test + public void shouldNotFailWhenApiClientIsFailed() { + // given + properties = givenOptableTargetingProperties(false); + final Query query = givenQuery(); + when(cache.get(any())).thenReturn(Future.failedFuture("empty")); + when(apiClient.getTargeting(any(), any(), any(), anyLong())).thenReturn(null); + + // when + final Future targetingResult = target.getTargeting(properties, query, + List.of("8.8.8.8"), 100); + + // then + assertThat(targetingResult.result()).isNull(); + } + + @Test + public void shouldNotFailWhenApiClientReturnsFailFuture() { + // given + properties = givenOptableTargetingProperties(false); + final Query query = givenQuery(); + when(cache.get(any())).thenReturn(Future.failedFuture("empty")); + when(apiClient.getTargeting(any(), any(), any(), anyLong())) + .thenReturn(Future.failedFuture("File")); + + // when + final Future targetingResult = target.getTargeting( + properties, query, List.of("8.8.8.8"), 100); + + // then + assertThat(targetingResult.result()).isNull(); + } +} From ae0b39f4937f25987d27469ffbd0a10e290934ba Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Fri, 13 Jun 2025 20:39:54 +0200 Subject: [PATCH 29/41] optable-targeting: Send GDPR and GPP consents at the same time --- .../config/OptableTargetingConfig.java | 2 - ...eTargetingProcessedAuctionRequestHook.java | 16 +++-- .../v1/core/OptableAttributesResolver.java | 68 ++++++------------- .../targeting/v1/net/CachedAPIClient.java | 3 +- .../optable/targeting/v1/BaseOptableTest.java | 17 +++++ .../v1/OptableTargetingModuleTest.java | 4 -- ...getingProcessedAuctionRequestHookTest.java | 3 - .../core/OptableAttributesResolverTest.java | 30 +++++--- 8 files changed, 71 insertions(+), 72 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 893ee91b5aa..98bf9c1443a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -133,14 +133,12 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, - OptableAttributesResolver optableAttributesResolver, UserFpdActivityMask userFpdActivityMask) { return new OptableTargetingModule(List.of( new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - optableAttributesResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( configResolver, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 86cfc978753..2daaefc2cf2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -12,6 +12,11 @@ import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.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.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionOutcome; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; @@ -31,7 +36,9 @@ import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import java.util.Collection; import java.util.Objects; +import java.util.Optional; import java.util.function.Function; public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { @@ -40,17 +47,14 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final ConfigResolver configResolver; private final OptableTargeting optableTargeting; - private final OptableAttributesResolver optableAttributesResolver; private final UserFpdActivityMask userFpdActivityMask; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, OptableTargeting optableTargeting, - OptableAttributesResolver optableAttributesResolver, UserFpdActivityMask userFpdActivityMask) { this.configResolver = Objects.requireNonNull(configResolver); this.optableTargeting = Objects.requireNonNull(optableTargeting); - this.optableAttributesResolver = Objects.requireNonNull(optableAttributesResolver); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); } @@ -64,12 +68,14 @@ public Future> call(AuctionRequestPayloa final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); final long timeout = getHookRemainingTime(invocationContext); - final OptableAttributes attributes = optableAttributesResolver.resolveAttributes( + final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), properties.getTimeout()); return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) - .compose(targetingResult -> enrichedPayload(targetingResult, moduleContext)) + .compose(targetingResult -> { + return enrichedPayload(targetingResult, moduleContext); + }) .recover(throwable -> { moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); return failure(BidRequestCleaner.instance(), moduleContext); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 81f057afcf4..0952c3fa5c4 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,9 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.SetUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; @@ -11,23 +11,32 @@ import org.prebid.server.privacy.model.PrivacyContext; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Optional; -import java.util.Set; public class OptableAttributesResolver { - public OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { - final List ips = resolveIp(auctionContext); + public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { + final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext(); + final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); - return Optional.ofNullable(getGdprPrivacyAttributes(auctionContext)) - .or(() -> Optional.ofNullable(getGppPrivacyAttributes(auctionContext))) - .map(OptableAttributes::toBuilder) - .orElseGet(OptableAttributes::builder) - .ips(ips) - .timeout(timeout) - .build(); + final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() + .ips(resolveIp(auctionContext)) + .timeout(timeout); + + if (tcfContext.isConsentValid()) { + builder + .gdprApplies(tcfContext.isInGdprScope()) + .gdprConsent(tcfContext.getConsentString()); + } + + if (gppScope.getGppModel() != null) { + builder + .gpp(gppScope.getGppModel().encode()) + .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); + } + + return builder.build(); } public static List resolveIp(AuctionContext auctionContext) { @@ -54,39 +63,4 @@ public static List resolveIp(AuctionContext auctionContext) { public static String resolveIp(List ips) { return CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none"; } - - private OptableAttributes getGppPrivacyAttributes(AuctionContext auctionContext) { - final Optional gppContextOpt = Optional.ofNullable(auctionContext) - .map(AuctionContext::getGppContext); - - final Optional gppScope = gppContextOpt - .map(GppContext::scope); - - final String gppConsent = gppScope.map(GppContext.Scope::getGppModel) - .map(GppModel::encode) - .orElse(null); - - if (gppConsent == null) { - return null; - } - - final Set sids = gppContextOpt - .map(GppContext::scope) - .map(GppContext.Scope::getSectionsIds) - .orElse(Collections.emptySet()); - - return OptableAttributes.builder().gpp(gppConsent).gppSid(sids).build(); - } - - private OptableAttributes getGdprPrivacyAttributes(AuctionContext auctionContext) { - return Optional.ofNullable(auctionContext) - .map(AuctionContext::getPrivacyContext) - .map(PrivacyContext::getTcfContext) - .filter(TcfContext::isConsentValid) - .map(ctx -> OptableAttributes.builder() - .gdprConsent(ctx.getConsentString()) - .gdprApplies(ctx.isInGdprScope()) - .build()) - .orElse(null); - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index 4349a3b79bb..73baeb73c78 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -6,6 +6,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -63,7 +64,7 @@ private String createCachingKey(String tenant, String origin, List ips, return "%s:%s:%s:%s".formatted( tenant, origin, - CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none", + OptableAttributesResolver.resolveIp(ips), encodeQuery ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) : query.getIds()); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index f62ae34a29a..c3caec2cb44 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; @@ -17,6 +18,7 @@ import org.apache.commons.io.IOUtils; import org.apache.http.HttpStatus; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.model.TimeoutContext; import org.prebid.server.execution.timeout.Timeout; @@ -32,6 +34,9 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.privacy.model.Privacy; +import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; @@ -43,8 +48,12 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.UnaryOperator; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + public abstract class BaseOptableTest { protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); @@ -62,9 +71,17 @@ protected ModuleContext givenModuleContext(List audiences) { } protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) { + final GppModel gppModel = new GppModel(); + final TcfContext tcfContext = TcfContext.builder().build(); + final GppContext gppContext = new GppContext( + GppContext.Scope.of(gppModel, Set.of(1)), + GppContext.Regions.builder().build()); + return AuctionContext.builder() .bidRequest(givenBidRequest()) .activityInfrastructure(activityInfrastructure) + .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) + .gppContext(gppContext) .timeoutContext(TimeoutContext.of(0, timeout, 1)) .build(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index c101f6c0c25..6172ae32622 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -32,9 +32,6 @@ public class OptableTargetingModuleTest { @Mock OptableTargeting optableTargeting; - @Mock - OptableAttributesResolver optableAttributesResolver; - @Mock(strictness = LENIENT) UserFpdActivityMask userFpdActivityMask; @@ -61,7 +58,6 @@ public void shouldReturnHooks() { List.of(new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - optableAttributesResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( configResolver, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 70e4db34d24..76cea879004 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -49,7 +49,6 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock ActivityInfrastructure activityInfrastructure; private ConfigResolver configResolver; - private OptableAttributesResolver optableAttributesResolver; private JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(new ObjectMapper())); private OptableTargetingProcessedAuctionRequestHook target; @@ -63,11 +62,9 @@ public void setUp() { when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); when(invocationContext.timeout()).thenReturn(timeout); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - optableAttributesResolver = new OptableAttributesResolver(); target = new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - optableAttributesResolver, userFpdActivityMask); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 7eaa8d04e9b..742e3e21c33 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -27,8 +27,6 @@ @ExtendWith(MockitoExtension.class) public class OptableAttributesResolverTest extends BaseOptableTest { - private OptableAttributesResolver target; - @Mock(strictness = LENIENT) private TcfContext tcfContext; @@ -44,19 +42,21 @@ public class OptableAttributesResolverTest extends BaseOptableTest { @BeforeEach public void setUp() { when(properties.getTimeout()).thenReturn(100L); - target = new OptableAttributesResolver(); } @Test public void shouldResolveTcfAttributesWhenConsentIsValid() { // given + final GppModel gppModel = mock(); when(tcfContext.isConsentValid()).thenReturn(true); when(tcfContext.isInGdprScope()).thenReturn(true); when(tcfContext.getConsentString()).thenReturn("consent"); - final AuctionContext auctionContext = givenAuctionContext(tcfContext); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); // when - final OptableAttributes result = target.resolveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() @@ -67,12 +67,15 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() { @Test public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // given + final GppModel gppModel = mock(); when(tcfContext.isConsentValid()).thenReturn(false); when(tcfContext.getConsentString()).thenReturn("consent"); - final AuctionContext auctionContext = givenAuctionContext(tcfContext); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); // when - final OptableAttributes result = target.resolveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() @@ -85,13 +88,14 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { public void shouldResolveGppAttributes() { // given final GppModel gppModel = mock(); + when(tcfContext.isConsentValid()).thenReturn(false); + when(tcfContext.getConsentString()).thenReturn("consent"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(gppContext); + final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); // when - - final OptableAttributes result = target.resolveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() @@ -110,6 +114,12 @@ public AuctionContext givenAuctionContext(GppContext gppContext) { return AuctionContext.builder().gppContext(gppContext).build(); } + public AuctionContext givenAuctionContext(TcfContext tcfContext, GppContext gppContext) { + return AuctionContext.builder() + .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) + .gppContext(gppContext).build(); + } + public AuctionContext givenAuctionContext(GeoInfo geoInfo) { return AuctionContext.builder().geoInfo(geoInfo).build(); } From 999f9718285a591afa58d86a81a80083f8908353 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sat, 14 Jun 2025 00:08:11 +0200 Subject: [PATCH 30/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 6 -- .../OptableTargetingAuctionResponseHook.java | 87 +------------------ ...eTargetingProcessedAuctionRequestHook.java | 14 +-- .../v1/core/AnalyticTagsResolver.java | 76 ++++++++++++++++ .../v1/core/OptableAttributesResolver.java | 3 + .../targeting/v1/net/CachedAPIClient.java | 1 - .../optable/targeting/v1/BaseOptableTest.java | 3 - ...tableTargetingAuctionResponseHookTest.java | 4 +- .../v1/OptableTargetingModuleTest.java | 1 - ...getingProcessedAuctionRequestHookTest.java | 26 +++++- .../core/OptableAttributesResolverTest.java | 9 +- 11 files changed, 121 insertions(+), 109 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 98bf9c1443a..dfdf0fcbc6a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -10,7 +10,6 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; @@ -104,11 +103,6 @@ APIClientFactory apiClientFactory( storageEnabled && moduleCacheEnabled); } - @Bean - OptableAttributesResolver optableAttributesResolver() { - return new OptableAttributesResolver(); - } - @Bean Cache cache(PbcStorageService cacheService, OptableResponseMapper responseMapper) { return new Cache(cacheService, responseMapper); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index fd32724b78a..44facef717d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -1,25 +1,15 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; import io.vertx.core.Future; import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.auction.model.AuctionContext; -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.Stage; -import org.prebid.server.hooks.execution.model.StageExecutionOutcome; 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.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; -import org.prebid.server.hooks.modules.optable.targeting.model.Reason; import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -27,27 +17,16 @@ 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.Result; -import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; import java.util.List; import java.util.Objects; -import java.util.Optional; public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { private static final String CODE = "optable-targeting-auction-response-hook"; - private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; - private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; - private static final String STATUS_EXECUTION_TIME = "execution-time"; - private static final String STATUS_REASON = "reason"; private final ConfigResolver configResolver; private final ObjectMapper objectMapper; @@ -68,7 +47,6 @@ public Future> call(AuctionResponsePayl final ModuleContext moduleContext = ModuleContext.of(invocationContext); moduleContext.setAdserverTargetingEnabled(adserverTargeting); - moduleContext.setOptableTargetingExecutionTime(extractOptableTargetingExecutionTime(invocationContext)); if (!adserverTargeting) { return success(moduleContext); @@ -83,26 +61,6 @@ public Future> call(AuctionResponsePayl : success(moduleContext); } - private long extractOptableTargetingExecutionTime(AuctionInvocationContext invocationContext) { - return Optional.ofNullable(invocationContext.auctionContext()) - .map(AuctionContext::getHookExecutionContext) - .map(HookExecutionContext::getStageOutcomes) - .map(stages -> stages.get(Stage.processed_auction_request)) - .stream() - .flatMap(Collection::stream) - .filter(stageExecutionOutcome -> "auction-request".equals(stageExecutionOutcome.getEntity())) - .map(StageExecutionOutcome::getGroups) - .flatMap(Collection::stream) - .map(GroupExecutionOutcome::getHooks) - .flatMap(Collection::stream) - .filter(hook -> OptableTargetingModule.CODE.equals(hook.getHookId().getModuleCode())) - .filter(hook -> - OptableTargetingProcessedAuctionRequestHook.CODE.equals(hook.getHookId().getHookImplCode())) - .findFirst() - .map(HookExecutionOutcome::getExecutionTime) - .orElse(0L); - } - private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); @@ -120,7 +78,7 @@ private Future> update( .action(InvocationAction.update) .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) - .analyticsTags(toAnalyticTags(moduleContext)) + .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) .build()); } @@ -130,49 +88,10 @@ private Future> success(ModuleContext m .status(InvocationStatus.success) .action(InvocationAction.no_action) .moduleContext(moduleContext) - .analyticsTags(toAnalyticTags(moduleContext)) + .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) .build()); } - private Tags toAnalyticTags(ModuleContext moduleContext) { - final String requestEnrichmentStatus = toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()); - final EnrichmentStatus responseEnrichmentStatus = moduleContext.getEnrichResponseStatus(); - final String responseEnrichmentStatusValue = toEnrichmentStatusValue(responseEnrichmentStatus); - final String responseEnrichmentStatusReason = toEnrichmentStatusReason(moduleContext.getEnrichResponseStatus()); - - final List activities = new ArrayList<>(); - activities.add(ActivityImpl.of(ACTIVITY_ENRICH_REQUEST, - requestEnrichmentStatus, - toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime())))); - - if (moduleContext.isAdserverTargetingEnabled()) { - activities.add(ActivityImpl.of(ACTIVITY_ENRICH_RESPONSE, - responseEnrichmentStatusValue, - toResults(STATUS_REASON, responseEnrichmentStatusReason))); - } - - return TagsImpl.of(activities); - } - - private String toEnrichmentStatusValue(EnrichmentStatus enrichRequestStatus) { - return Optional.ofNullable(enrichRequestStatus) - .map(EnrichmentStatus::getStatus) - .map(Status::getValue) - .orElse(null); - } - - private String toEnrichmentStatusReason(EnrichmentStatus enrichmentStatus) { - return Optional.ofNullable(enrichmentStatus) - .map(EnrichmentStatus::getReason) - .map(Reason::getValue) - .orElse(null); - } - - private List toResults(String result, String value) { - final ObjectNode resultDetails = objectMapper.createObjectNode().put(result, value); - return Collections.singletonList(ResultImpl.of(null, resultDetails, null)); - } - @Override public String code() { return CODE; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 2daaefc2cf2..83227a0a4c0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -12,17 +12,13 @@ import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; -import org.prebid.server.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.Stage; -import org.prebid.server.hooks.execution.model.StageExecutionOutcome; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -36,9 +32,7 @@ import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; -import java.util.Collection; import java.util.Objects; -import java.util.Optional; import java.util.function.Function; public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { @@ -72,11 +66,16 @@ public Future> call(AuctionRequestPayloa invocationContext.auctionContext(), properties.getTimeout()); + final long callTargetingAPITimestamp = System.currentTimeMillis(); return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) .compose(targetingResult -> { + moduleContext.setOptableTargetingExecutionTime( + System.currentTimeMillis() - callTargetingAPITimestamp); return enrichedPayload(targetingResult, moduleContext); }) .recover(throwable -> { + moduleContext.setOptableTargetingExecutionTime( + System.currentTimeMillis() - callTargetingAPITimestamp); moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); return failure(BidRequestCleaner.instance(), moduleContext); }); @@ -139,6 +138,7 @@ private static Future> update( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) .payloadUpdate(payloadUpdate::apply) .moduleContext(moduleContext) .build()); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java new file mode 100644 index 00000000000..36681dbfdfc --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java @@ -0,0 +1,76 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +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.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Reason; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +public class AnalyticTagsResolver { + + private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; + private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; + private static final String STATUS_EXECUTION_TIME = "execution-time"; + private static final String STATUS_REASON = "reason"; + + private AnalyticTagsResolver() { + } + + public static Tags toEnrichRequestAnalyticTags(ModuleContext moduleContext) { + final String requestEnrichmentStatus = toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()); + + final List activities = new ArrayList<>(); + activities.add(ActivityImpl.of(ACTIVITY_ENRICH_REQUEST, + requestEnrichmentStatus, + toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime())))); + + return TagsImpl.of(activities); + } + + public static Tags toEnrichResponseAnalyticTags(ModuleContext moduleContext) { + final EnrichmentStatus responseEnrichmentStatus = moduleContext.getEnrichResponseStatus(); + final String responseEnrichmentStatusValue = toEnrichmentStatusValue(responseEnrichmentStatus); + final String responseEnrichmentStatusReason = toEnrichmentStatusReason(moduleContext.getEnrichResponseStatus()); + + final List activities = new ArrayList<>(); + + if (moduleContext.isAdserverTargetingEnabled()) { + activities.add(ActivityImpl.of(ACTIVITY_ENRICH_RESPONSE, + responseEnrichmentStatusValue, + toResults(STATUS_REASON, responseEnrichmentStatusReason))); + } + + return TagsImpl.of(activities); + } + + private static String toEnrichmentStatusValue(EnrichmentStatus enrichRequestStatus) { + return Optional.ofNullable(enrichRequestStatus) + .map(EnrichmentStatus::getStatus) + .map(Status::getValue) + .orElse(null); + } + + private static String toEnrichmentStatusReason(EnrichmentStatus enrichmentStatus) { + return Optional.ofNullable(enrichmentStatus) + .map(EnrichmentStatus::getReason) + .map(Reason::getValue) + .orElse(null); + } + + private static List toResults(String result, String value) { + final ObjectNode resultDetails = ObjectMapperProvider.mapper().createObjectNode().put(result, value); + return Collections.singletonList(ResultImpl.of(null, resultDetails, null)); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 0952c3fa5c4..3ac6881acee 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -16,6 +16,9 @@ public class OptableAttributesResolver { + private OptableAttributesResolver() { + } + public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext(); final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index 73baeb73c78..8c39f07e87b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -1,7 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; import io.vertx.core.Future; -import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index c3caec2cb44..b35e6f0f714 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -51,9 +51,6 @@ import java.util.Set; import java.util.function.UnaryOperator; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - public abstract class BaseOptableTest { protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index b3c9b0b5cf6..60f479771b3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -60,7 +60,7 @@ public void shouldHaveCode() { } @Test - public void shouldReturnResultWithNoActionAndPBSAnalyticsTags() { + public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); @@ -77,7 +77,7 @@ public void shouldReturnResultWithNoActionAndPBSAnalyticsTags() { assertThat(result.status()).isEqualTo(InvocationStatus.success); assertThat(result.action()).isEqualTo(InvocationAction.no_action); assertThat(result.analyticsTags().activities().getFirst() - .results().getFirst().values().get("execution-time")).isNotNull(); + .results().getFirst().values().get("reason")).isNotNull(); assertThat(result.errors()).isNull(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 6172ae32622..9e283f248d8 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -7,7 +7,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.v1.Hook; import org.prebid.server.hooks.v1.InvocationContext; diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 76cea879004..a26faa7a26c 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -16,7 +16,6 @@ import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -74,6 +73,30 @@ public void shouldHaveRightCode() { assertThat(target.code()).isEqualTo("optable-targeting-processed-auction-request-hook"); } + @Test + public void shouldReturnResultWithPBSAnalyticsTags() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + assertThat(result.analyticsTags().activities().getFirst() + .results().getFirst().values().get("execution-time")).isNotNull(); + } + @Test public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() { // given @@ -100,7 +123,6 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin .bidRequest(); assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); - } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 742e3e21c33..d82f932cf43 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -56,7 +56,8 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() { final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); // when - final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, + properties.getTimeout()); // then assertThat(result).isNotNull() @@ -75,7 +76,8 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); // when - final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, + properties.getTimeout()); // then assertThat(result).isNotNull() @@ -95,7 +97,8 @@ public void shouldResolveGppAttributes() { final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); // when - final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, + properties.getTimeout()); // then assertThat(result).isNotNull() From 10eeb11d9dc12bc1093ecb0ce8d331d1ddd428b3 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sat, 14 Jun 2025 00:30:41 +0200 Subject: [PATCH 31/41] optable-targeting: up pbs version --- extra/modules/optable-targeting/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index 34726f24487..49e9ca94f9c 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.27.0-SNAPSHOT + 3.28.0-SNAPSHOT optable-targeting From a24f0ccb9a1768ae4b18b276f9d64ac2ba61d722 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Tue, 24 Jun 2025 23:03:56 +0200 Subject: [PATCH 32/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 8 +-- .../targeting/model/net/OptableCall.java | 8 --- .../OptableTargetingAuctionResponseHook.java | 9 ++-- ...eTargetingProcessedAuctionRequestHook.java | 18 +++---- .../v1/core/AnalyticTagsResolver.java | 26 ++++------ .../targeting/v1/core/BidRequestCleaner.java | 50 ++++++++----------- .../v1/core/merger/PayloadCleaner.java | 17 ------- 7 files changed, 45 insertions(+), 91 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index dfdf0fcbc6a..06f77faab00 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -69,10 +69,10 @@ OptableHttpClientWrapper optableHttpClient(Vertx vertx, HttpClientProperties htt @Bean(name = "apiClient") APIClient apiClientImpl(OptableHttpClientWrapper httpClientWrapper, - @Value("${logging.sampling-rate:0.01}") - double logSamplingRate, - OptableTargetingProperties properties, - OptableResponseMapper responseParser) { + @Value("${logging.sampling-rate:0.01}") + double logSamplingRate, + OptableTargetingProperties properties, + OptableResponseMapper responseParser) { return new APIClientImpl( properties.getApiEndpoint(), diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java index f80d6db1ab2..0f41164680c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java @@ -15,12 +15,4 @@ public class OptableCall { public static OptableCall succeededHttp(HttpRequest request, HttpResponse response) { return new OptableCall(request, response); } - - public static OptableCall failedHttp(HttpRequest request) { - return new OptableCall(request, null); - } - - public static OptableCall failedHttp(HttpResponse response) { - return new OptableCall(null, response); - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 44facef717d..321930541e5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -27,13 +27,11 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook { private static final String CODE = "optable-targeting-auction-response-hook"; + private final ConfigResolver configResolver; private final ObjectMapper objectMapper; - public OptableTargetingAuctionResponseHook( - ConfigResolver configResolver, - ObjectMapper objectMapper) { - + public OptableTargetingAuctionResponseHook(ConfigResolver configResolver, ObjectMapper objectMapper) { this.configResolver = Objects.requireNonNull(configResolver); this.objectMapper = Objects.requireNonNull(objectMapper); } @@ -70,7 +68,8 @@ private Future> enrichedPayload(ModuleC } private Future> update( - PayloadUpdate payloadUpdate, ModuleContext moduleContext) { + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { return Future.succeededFuture( InvocationResultImpl.builder() diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 83227a0a4c0..19a0e0e06d5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -33,7 +33,6 @@ import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; import java.util.Objects; -import java.util.function.Function; public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { @@ -77,7 +76,7 @@ public Future> call(AuctionRequestPayloa moduleContext.setOptableTargetingExecutionTime( System.currentTimeMillis() - callTargetingAPITimestamp); moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); - return failure(BidRequestCleaner.instance(), moduleContext); + return update(BidRequestCleaner.instance(), moduleContext); }); } @@ -126,12 +125,14 @@ private Future> enrichedPayload(Targetin moduleContext.setTargeting(targetingResult.getAudience()); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); return update( - BidRequestEnricher.of(targetingResult).compose(BidRequestCleaner.instance()), + BidRequestCleaner.instance() + .andThen(BidRequestEnricher.of(targetingResult)) + ::apply, moduleContext); } private static Future> update( - Function payloadUpdate, + PayloadUpdate payloadUpdate, ModuleContext moduleContext) { return Future.succeededFuture( @@ -139,18 +140,11 @@ private static Future> update( .status(InvocationStatus.success) .action(InvocationAction.update) .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) - .payloadUpdate(payloadUpdate::apply) + .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) .build()); } - private static Future> failure( - PayloadUpdate payloadUpdate, - ModuleContext moduleContext) { - - return update(payloadUpdate, moduleContext); - } - @Override public String code() { return CODE; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java index 36681dbfdfc..80905ce088c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AnalyticTagsResolver.java @@ -29,30 +29,22 @@ private AnalyticTagsResolver() { } public static Tags toEnrichRequestAnalyticTags(ModuleContext moduleContext) { - final String requestEnrichmentStatus = toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()); - - final List activities = new ArrayList<>(); - activities.add(ActivityImpl.of(ACTIVITY_ENRICH_REQUEST, - requestEnrichmentStatus, - toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime())))); - - return TagsImpl.of(activities); + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY_ENRICH_REQUEST, + toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()), + toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime()))))); } public static Tags toEnrichResponseAnalyticTags(ModuleContext moduleContext) { - final EnrichmentStatus responseEnrichmentStatus = moduleContext.getEnrichResponseStatus(); - final String responseEnrichmentStatusValue = toEnrichmentStatusValue(responseEnrichmentStatus); - final String responseEnrichmentStatusReason = toEnrichmentStatusReason(moduleContext.getEnrichResponseStatus()); - final List activities = new ArrayList<>(); - if (moduleContext.isAdserverTargetingEnabled()) { - activities.add(ActivityImpl.of(ACTIVITY_ENRICH_RESPONSE, - responseEnrichmentStatusValue, - toResults(STATUS_REASON, responseEnrichmentStatusReason))); + activities.add(ActivityImpl.of( + ACTIVITY_ENRICH_RESPONSE, + toEnrichmentStatusValue(moduleContext.getEnrichResponseStatus()), + toResults(STATUS_REASON, toEnrichmentStatusReason(moduleContext.getEnrichResponseStatus())))); } - return TagsImpl.of(activities); + return TagsImpl.of(Collections.unmodifiableList(activities)); } private static String toEnrichmentStatusValue(EnrichmentStatus enrichRequestStatus) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java index e25ba02b0c5..30652383942 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java @@ -4,52 +4,46 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.User; -import lombok.Value; -import lombok.experimental.Accessors; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.PayloadCleaner; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.proto.openrtb.ext.request.ExtUser; -import java.util.Optional; +import java.util.List; -@Accessors(fluent = true) -@Value(staticConstructor = "instance") public class BidRequestCleaner implements PayloadUpdate { + private static final String OPTABLE_FIELD = "optable"; + private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid"); + + public static BidRequestCleaner instance() { + return new BidRequestCleaner(); + } + @Override public AuctionRequestPayload apply(AuctionRequestPayload payload) { return AuctionRequestPayloadImpl.of(clearExtUserOptable(payload.bidRequest())); } private static BidRequest clearExtUserOptable(BidRequest bidRequest) { - final Optional userOpt = getUserOpt(bidRequest); - - final JsonNode userExtOptable = userOpt.map(User::getExt) - .map(it -> it.getProperty("optable")) - .map(it -> PayloadCleaner.cleanUserExtOptable((ObjectNode) it)) - .orElse(null); - - if (userExtOptable != null) { - final ExtUser extUser = userOpt.map(User::getExt).orElse(null); - if (!userExtOptable.isEmpty()) { - extUser.addProperty("optable", userExtOptable); - } else { - extUser.addProperty("optable", null); - } - return bidRequest.toBuilder() - .user(userOpt.get().toBuilder() - .ext(extUser) - .build()) - .build(); + final User user = bidRequest.getUser(); + final ExtUser extUser = user != null ? user.getExt() : null; + final JsonNode optable = extUser != null ? extUser.getProperty(OPTABLE_FIELD) : null; + if (optable == null || !optable.isObject() || optable.isEmpty()) { + return bidRequest; + } + + final ObjectNode cleanedOptable = cleanOptable((ObjectNode) optable); + if (cleanedOptable.isEmpty()) { + extUser.addProperty(OPTABLE_FIELD, null); + } else { + extUser.addProperty(OPTABLE_FIELD, cleanedOptable); } return bidRequest; } - private static Optional getUserOpt(BidRequest bidRequest) { - return Optional.ofNullable(bidRequest) - .map(BidRequest::getUser); + public static ObjectNode cleanOptable(ObjectNode optable) { + return optable.deepCopy().remove(FIELDS_TO_REMOVE); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java deleted file mode 100644 index fe74f1a05c4..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/PayloadCleaner.java +++ /dev/null @@ -1,17 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.util.List; - -public class PayloadCleaner { - - private static final List FIELDS_FILTER = List.of("email", "phone", "zip", "vid"); - - private PayloadCleaner() { - } - - public static ObjectNode cleanUserExtOptable(ObjectNode optable) { - return optable.deepCopy().remove(FIELDS_FILTER); - } -} From 917ef61e44d6bfc3964d66ad33092385ae11e98e Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Wed, 25 Jun 2025 18:50:10 +0200 Subject: [PATCH 33/41] optable-targeting: replace custom Data object by existing one --- .../config/OptableTargetingConfig.java | 6 +- .../optable/targeting/model/openrtb/Data.java | 13 ----- .../targeting/model/openrtb/Segment.java | 12 ---- .../optable/targeting/model/openrtb/User.java | 1 + ...eTargetingProcessedAuctionRequestHook.java | 6 +- .../targeting/v1/core/BidRequestEnricher.java | 2 +- .../targeting/v1/core/merger/DataMerger.java | 51 ++++------------- .../optable/targeting/v1/BaseOptableTest.java | 48 ++++++++++++---- .../v1/OptableTargetingModuleTest.java | 6 +- ...getingProcessedAuctionRequestHookTest.java | 3 +- .../v1/core/merger/DataMergerTest.java | 55 ++++++++++--------- 11 files changed, 95 insertions(+), 108 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 06f77faab00..07bff81725d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -127,13 +127,15 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask) { + UserFpdActivityMask userFpdActivityMask, + JsonMerger jsonMerger) { return new OptableTargetingModule(List.of( new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - userFpdActivityMask), + userFpdActivityMask, + jsonMerger), new OptableTargetingAuctionResponseHook( configResolver, ObjectMapperProvider.mapper()))); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java deleted file mode 100644 index 7020cc430d2..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Data.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; - -import lombok.Value; - -import java.util.List; - -@Value -public class Data { - - String id; - - List segment; -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java deleted file mode 100644 index b66238a661d..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/Segment.java +++ /dev/null @@ -1,12 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; - -import com.fasterxml.jackson.databind.node.ObjectNode; -import lombok.Value; - -@Value -public class Segment { - - String id; - - ObjectNode ext; -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java index 92f4df77101..1ad2cdb220b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/User.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; +import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Eid; import lombok.Value; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 19a0e0e06d5..f26b01564f7 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -31,6 +31,7 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; +import org.prebid.server.json.JsonMerger; import java.util.Objects; @@ -41,14 +42,17 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final ConfigResolver configResolver; private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; + private final JsonMerger jsonMerger; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask) { + UserFpdActivityMask userFpdActivityMask, + JsonMerger jsonMerger) { this.configResolver = Objects.requireNonNull(configResolver); this.optableTargeting = Objects.requireNonNull(optableTargeting); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.jsonMerger = Objects.requireNonNull(jsonMerger); } @Override diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index 9d206b508fa..06d18432cc5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -1,12 +1,12 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Eid; import lombok.Value; import lombok.experimental.Accessors; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java index 399cd02e5c6..5fd64d8c595 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java @@ -1,8 +1,8 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; +import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Segment; import org.apache.commons.collections4.CollectionUtils; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; import java.util.List; import java.util.Map; @@ -14,47 +14,46 @@ public class DataMerger { private DataMerger() { } - public static List merge(List destination, + public static List merge(List destination, List source) { if (CollectionUtils.isEmpty(source)) { return destination; } - final Map idToData = mapDataToId(destination); + final Map idToData = mapDataToId(destination); if (idToData == null || idToData.isEmpty()) { - return source.stream().map(DataMerger::toData).toList(); + return source; } source.forEach(data -> idToData.compute(data.getId(), (id, item) -> item != null ? mergeData(item, data) - : toData(data))); + : data)); return idToData.values().stream().toList(); } - private static com.iab.openrtb.request.Data mergeData(com.iab.openrtb.request.Data destination, Data source) { + private static Data mergeData(Data destination, Data source) { if (source == null) { return destination; } final Map idToSegment = mapSegmentToId(destination.getSegment()); if (idToSegment == null) { - return toData(source); + return source; } Optional.ofNullable(source.getSegment()).ifPresent(it -> it.forEach(seg -> idToSegment.compute(seg.getId(), (id, item) -> item != null ? mergeSegment(item, seg) - : toSegment(seg)))); + : seg))); return destination.toBuilder() .segment(idToSegment.values().stream().toList()) .build(); } - private static Segment mergeSegment(Segment destination, - org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment source) { + private static Segment mergeSegment(Segment destination, Segment source) { return Segment.builder() .id(destination.getId()) @@ -64,43 +63,15 @@ private static Segment mergeSegment(Segment destination, .build(); } - private static Segment toSegment(org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment segment) { - return Segment.builder() - .id(segment.getId()) - .ext(segment.getExt()) - .build(); - } - private static Map mapSegmentToId(List segment) { return CollectionUtils.isNotEmpty(segment) ? segment.stream().collect(Collectors.toMap(Segment::getId, it -> it)) : null; } - private static com.iab.openrtb.request.Data toData(Data data) { - if (data == null) { - return null; - } - - final List segment = Optional.of(data) - .map(Data::getSegment) - .map(it -> it.stream() - .map(seg -> Segment.builder() - .id(seg.getId()) - .ext(seg.getExt()) - .build()) - .toList()) - .orElse(null); - - return com.iab.openrtb.request.Data.builder() - .id(data.getId()) - .segment(segment) - .build(); - } - - private static Map mapDataToId(List data) { + private static Map mapDataToId(List data) { return CollectionUtils.isNotEmpty(data) - ? data.stream().collect(Collectors.toMap(com.iab.openrtb.request.Data::getId, it -> it)) + ? data.stream().collect(Collectors.toMap(Data::getId, it -> it)) : null; } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index b35e6f0f714..cd049326862 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -5,9 +5,11 @@ import com.fasterxml.jackson.databind.node.TextNode; import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; +import com.iab.openrtb.request.Segment; import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; @@ -29,10 +31,10 @@ import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.Privacy; @@ -55,6 +57,8 @@ public abstract class BaseOptableTest { protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); + protected final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); + protected ModuleContext givenModuleContext() { return givenModuleContext(null); } @@ -84,8 +88,12 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr } protected BidRequest givenBidRequest() { + return givenBidRequest((List) null); + } + + protected BidRequest givenBidRequest(List eids) { return BidRequest.builder() - .user(givenUser()) + .user(givenUser(eids)) .device(givenDevice()) .cur(List.of("USD")) .build(); @@ -113,7 +121,26 @@ protected BidResponse givenBidResponse() { .build(); } + protected TargetingResult givenTargetingResultWithEids(List eids) { + return givenTargetingResult(eids, null); + } + protected TargetingResult givenTargetingResult() { + return givenTargetingResult(List.of(Eid.builder() + .source("source") + .uids(List.of(Uid.builder() + .id("id") + .build())) + .build()), + List.of(Data.builder() + .id("id") + .segment(List.of(Segment.builder() + .id("id") + .build())) + .build())); + } + + protected TargetingResult givenTargetingResult(List eids, List data) { return new TargetingResult( List.of(new Audience( "provider", @@ -122,15 +149,7 @@ protected TargetingResult givenTargetingResult() { 1 )), new Ortb2( - new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User( - List.of(Eid.builder() - .source("source") - .uids(List.of(Uid.builder() - .id("id") - .build())) - .build()), - List.of(new Data("id", List.of(new Segment("id", null)))) - ) + new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data) ) ); } @@ -140,6 +159,10 @@ protected TargetingResult givenEmptyTargetingResult() { } protected User givenUser() { + return givenUser(null); + } + + protected User givenUser(List eids) { final ObjectNode optable = mapper.createObjectNode(); optable.set("email", TextNode.valueOf("email")); optable.set("phone", TextNode.valueOf("phone")); @@ -150,6 +173,7 @@ protected User givenUser() { extUser.addProperty("optable", optable); return User.builder() + .eids(eids) .geo(Geo.builder().country("country-u").region("region-u").build()) .ext(extUser) .build(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 9e283f248d8..5f6cb4f7d3a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -11,6 +11,8 @@ 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.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import java.util.Collection; @@ -35,6 +37,7 @@ public class OptableTargetingModuleTest { UserFpdActivityMask userFpdActivityMask; ObjectMapper mapper = ObjectMapperProvider.mapper(); + JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); @Test public void shouldReturnNonBlankCode() { @@ -57,7 +60,8 @@ public void shouldReturnHooks() { List.of(new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - userFpdActivityMask), + userFpdActivityMask, + jsonMerger), new OptableTargetingAuctionResponseHook( configResolver, mapper)); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index a26faa7a26c..fab9caeaa3b 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -64,7 +64,8 @@ public void setUp() { target = new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - userFpdActivityMask); + userFpdActivityMask, + jsonMerger); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java index b21267f0d72..e9bdeeb80c3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java @@ -1,8 +1,8 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Segment; import org.junit.jupiter.api.Test; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Data; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Segment; import java.util.List; import java.util.Map; @@ -15,24 +15,24 @@ public class DataMergerTest extends BaseMergerTest { @Test public void shouldMergeDifferentData() { // given - final List destination = givenORTBData("dataId1", "segmentId1", + final List destination = givenORTBData("dataId1", "segmentId1", "field1", "value1"); final List source = givenOptableData("dataId2", "segmentId2", "field2", "value2"); // when - final List result = DataMerger.merge(destination, source); + final List result = DataMerger.merge(destination, source); // then assertThat(result).isNotNull() .hasSize(2); assertThat(result.getFirst()) - .returns("dataId2", from(com.iab.openrtb.request.Data::getId)) + .returns("dataId2", from(Data::getId)) .returns("segmentId2", it -> it.getSegment().getFirst().getId()) .returns("value2", it -> it.getSegment().getFirst().getExt().get("field2").asText()); assertThat(result.get(1)) - .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("dataId1", from(Data::getId)) .returns("segmentId1", it -> it.getSegment().getFirst().getId()) .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); } @@ -40,19 +40,19 @@ public void shouldMergeDifferentData() { @Test public void shouldMergeSegmentsWithinTheSameData() { // given - final List destination = givenORTBData("dataId1", "segmentId1", + final List destination = givenORTBData("dataId1", "segmentId1", "field1", "value1"); final List source = givenOptableData("dataId1", "segmentId2", "field2", "value2"); // when - final List result = DataMerger.merge(destination, source); + final List result = DataMerger.merge(destination, source); // then assertThat(result).isNotNull() .hasSize(1); assertThat(result.getFirst()) - .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("dataId1", from(Data::getId)) .returns("segmentId1", it -> it.getSegment().getFirst().getId()) .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()) .returns("segmentId2", it -> it.getSegment().get(1).getId()) @@ -62,19 +62,19 @@ public void shouldMergeSegmentsWithinTheSameData() { @Test public void shouldMergeExtWithinTheSameSegment() { // given - final List destination = givenORTBData("dataId1", "segmentId1", + final List destination = givenORTBData("dataId1", "segmentId1", "field1", "value1"); final List source = givenOptableData("dataId1", "segmentId1", "field2", "value2"); // when - final List result = DataMerger.merge(destination, source); + final List result = DataMerger.merge(destination, source); // then assertThat(result).isNotNull() .hasSize(1); assertThat(result.getFirst()) - .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("dataId1", from(Data::getId)) .returns("segmentId1", it -> it.getSegment().getFirst().getId()) .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()) .returns("value2", it -> it.getSegment().getFirst().getExt().get("field2").asText()); @@ -86,14 +86,14 @@ public void shouldUseFirstArgumentWhenSecondIsAbsent() { final List source = givenOptableData("dataId1", "segmentId1", "field1", "value1"); // when - final List result = DataMerger.merge(null, source); + final List result = DataMerger.merge(null, source); // then assertThat(result).isNotNull() .hasSize(1); assertThat(result.getFirst()) - .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("dataId1", from(Data::getId)) .returns("segmentId1", it -> it.getSegment().getFirst().getId()) .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); } @@ -101,17 +101,17 @@ public void shouldUseFirstArgumentWhenSecondIsAbsent() { @Test public void shouldUseSecondArgumentWhenFirstIsAbsent() { // given - final List destination = givenORTBData("dataId1", "segmentId1", + final List destination = givenORTBData("dataId1", "segmentId1", "field1", "value1"); // when - final List result = DataMerger.merge(destination, null); + final List result = DataMerger.merge(destination, null); // then assertThat(result).isNotNull() .hasSize(1); assertThat(result.getFirst()) - .returns("dataId1", from(com.iab.openrtb.request.Data::getId)) + .returns("dataId1", from(Data::getId)) .returns("segmentId1", it -> it.getSegment().getFirst().getId()) .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); } @@ -119,24 +119,29 @@ public void shouldUseSecondArgumentWhenFirstIsAbsent() { @Test public void shouldNotFailWhenArgumentsAreAbsent() { // given and when - final List result = DataMerger.merge(null, null); + final List result = DataMerger.merge(null, null); // then assertThat(result).isNull(); } private List givenOptableData(String id, String segmentId, String extField, String extValue) { - return List.of(new Data(id, List.of(new Segment(segmentId, givenExt(Map.of(extField, extValue)))))); + return List.of(Data.builder() + .id(id) + .segment(List.of(Segment.builder() + .id(segmentId) + .ext(givenExt(Map.of(extField, extValue))) + .build())) + .build()); } - private List givenORTBData(String id, String segmentId, String extField, - String extValue) { + private List givenORTBData(String id, String segmentId, String extField, String extValue) { - return List.of(com.iab.openrtb.request.Data.builder() + return List.of(Data.builder() .id(id) - .segment(List.of(com.iab.openrtb.request.Segment.builder() - .id(segmentId) - .ext(givenExt(Map.of(extField, extValue))) + .segment(List.of(Segment.builder() + .id(segmentId) + .ext(givenExt(Map.of(extField, extValue))) .build())) .build()); } From d8fcbc5e289904f7b8c238b9d24235f928c4aab2 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 26 Jun 2025 10:29:35 +0200 Subject: [PATCH 34/41] optable-targeting: Update user data and eids merging algorithms --- .../config/OptableTargetingConfig.java | 6 +- ...eTargetingProcessedAuctionRequestHook.java | 6 +- .../targeting/v1/core/BidRequestEnricher.java | 121 +++++-- .../targeting/v1/core/merger/DataMerger.java | 77 ----- .../targeting/v1/core/merger/EidsMerger.java | 74 ----- .../optable/targeting/v1/BaseOptableTest.java | 30 +- .../v1/OptableTargetingModuleTest.java | 6 +- ...getingProcessedAuctionRequestHookTest.java | 3 +- .../v1/core/BidRequestEnricherTest.java | 309 +++++++++++++++++- .../v1/core/merger/BaseMergerTest.java | 18 - .../v1/core/merger/DataMergerTest.java | 148 --------- .../v1/core/merger/EidsMergerTest.java | 125 ------- .../v1/core/merger/ExtMergerTest.java | 12 +- 13 files changed, 431 insertions(+), 504 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 07bff81725d..06f77faab00 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -127,15 +127,13 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - JsonMerger jsonMerger) { + UserFpdActivityMask userFpdActivityMask) { return new OptableTargetingModule(List.of( new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - userFpdActivityMask, - jsonMerger), + userFpdActivityMask), new OptableTargetingAuctionResponseHook( configResolver, ObjectMapperProvider.mapper()))); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index f26b01564f7..19a0e0e06d5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -31,7 +31,6 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; -import org.prebid.server.json.JsonMerger; import java.util.Objects; @@ -42,17 +41,14 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final ConfigResolver configResolver; private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; - private final JsonMerger jsonMerger; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - JsonMerger jsonMerger) { + UserFpdActivityMask userFpdActivityMask) { this.configResolver = Objects.requireNonNull(configResolver); this.optableTargeting = Objects.requireNonNull(optableTargeting); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); - this.jsonMerger = Objects.requireNonNull(jsonMerger); } @Override diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index 06d18432cc5..c142e5a3053 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -3,81 +3,136 @@ import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Eid; -import lombok.Value; -import lombok.experimental.Accessors; +import com.iab.openrtb.request.Segment; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.DataMerger; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.EidsMerger; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import java.util.ArrayList; +import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; -@Accessors(fluent = true) -@Value(staticConstructor = "of") public class BidRequestEnricher implements PayloadUpdate { - TargetingResult targetingResult; + private final TargetingResult targetingResult; + + private BidRequestEnricher(TargetingResult targetingResult) { + this.targetingResult = targetingResult; + } + + public static BidRequestEnricher of(TargetingResult targetingResult) { + return new BidRequestEnricher(targetingResult); + } @Override public AuctionRequestPayload apply(AuctionRequestPayload payload) { - return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest(), targetingResult)); + return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest())); } - private static BidRequest enrichBidRequest(BidRequest bidRequest, TargetingResult targetingResults) { - if (bidRequest == null || targetingResults == null) { + private BidRequest enrichBidRequest(BidRequest bidRequest) { + if (bidRequest == null || targetingResult == null) { return bidRequest; } - final User optableUser = getUser(targetingResults); + final User optableUser = Optional.of(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .orElse(null); + if (optableUser == null) { return bidRequest; } - final com.iab.openrtb.request.User bidRequestUser = getOrCreateUser(bidRequest); + final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser()) + .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); return bidRequest.toBuilder() .user(mergeUserData(bidRequestUser, optableUser)) .build(); } - private static com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { - final com.iab.openrtb.request.User.UserBuilder userBuilder = user.toBuilder(); + private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { + return user.toBuilder() + .eids(mergeEids(user.getEids(), optableUser.getEids())) + .data(mergeData(user.getData(), optableUser.getData())) + .build(); + } - final List eids = optableUser.getEids(); - final List data = optableUser.getData(); + private List mergeEids(List destination, List source) { + return merge( + destination, + source, + Eid::getSource); + } - if (!CollectionUtils.isEmpty(eids)) { - userBuilder.eids(EidsMerger.merge(user.getEids(), eids)); + private List mergeData(List destination, List source) { + if (CollectionUtils.isEmpty(destination)) { + return source; } - if (!CollectionUtils.isEmpty(data)) { - userBuilder.data(DataMerger.merge(user.getData(), data)); + if (CollectionUtils.isEmpty(source)) { + return destination; } - return userBuilder.build(); + final Map idToSourceData = source.stream() + .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, LinkedHashMap::new)); + + final Set mergedDataIds = new HashSet<>(); + + final Stream mergedData = destination.stream() + .map(destinationData -> mergeDataEntry(destinationData, idToSourceData, mergedDataIds)); + + return Stream.concat(mergedData, source.stream().filter(it -> !mergedDataIds.contains(it.getId()))).toList(); } - private static User getUser(TargetingResult targetingResults) { - return Optional.ofNullable(targetingResults) - .map(TargetingResult::getOrtb2) - .map(Ortb2::getUser) - .orElse(null); + private Data mergeData(Data destinationData, Data sourceData) { + return Data.builder() + .id(destinationData.getId()) + .name(destinationData.getName()) + .ext(destinationData.getExt()) + .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId)) + .build(); } - private static com.iab.openrtb.request.User getOrCreateUser(BidRequest bidRequest) { - return getUserOpt(bidRequest) - .orElseGet(() -> com.iab.openrtb.request.User.builder().eids(new ArrayList<>()).build()); + private Data mergeDataEntry(Data destinationData, Map idToSourceData, Set mergedDataIds) { + return Optional.ofNullable(idToSourceData.get(destinationData.getId())) + .map(sourceData -> { + mergedDataIds.add(sourceData.getId()); + return mergeData(destinationData, sourceData); + }) + .orElse(destinationData); } - private static Optional getUserOpt(BidRequest bidRequest) { - return Optional.ofNullable(bidRequest) - .map(BidRequest::getUser); + private static List merge(List destination, + List source, + Function idExtractor) { + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + final Set existingIds = destination.stream() + .map(idExtractor) + .collect(Collectors.toSet()); + + final List uniqueFromSource = source.stream() + .filter(entry -> !existingIds.contains(idExtractor.apply(entry))) + .toList(); + + return Stream.concat(destination.stream(), uniqueFromSource.stream()).toList(); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java deleted file mode 100644 index 5fd64d8c595..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMerger.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.iab.openrtb.request.Data; -import com.iab.openrtb.request.Segment; -import org.apache.commons.collections4.CollectionUtils; - -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -public class DataMerger { - - private DataMerger() { - } - - public static List merge(List destination, - List source) { - - if (CollectionUtils.isEmpty(source)) { - return destination; - } - - final Map idToData = mapDataToId(destination); - if (idToData == null || idToData.isEmpty()) { - return source; - } - - source.forEach(data -> idToData.compute(data.getId(), (id, item) -> item != null - ? mergeData(item, data) - : data)); - - return idToData.values().stream().toList(); - } - - private static Data mergeData(Data destination, Data source) { - if (source == null) { - return destination; - } - - final Map idToSegment = mapSegmentToId(destination.getSegment()); - if (idToSegment == null) { - return source; - } - - Optional.ofNullable(source.getSegment()).ifPresent(it -> - it.forEach(seg -> idToSegment.compute(seg.getId(), (id, item) -> item != null - ? mergeSegment(item, seg) - : seg))); - - return destination.toBuilder() - .segment(idToSegment.values().stream().toList()) - .build(); - } - - private static Segment mergeSegment(Segment destination, Segment source) { - - return Segment.builder() - .id(destination.getId()) - .value(destination.getValue()) - .name(destination.getName()) - .ext(ExtMerger.mergeExt(destination.getExt(), source.getExt())) - .build(); - } - - private static Map mapSegmentToId(List segment) { - return CollectionUtils.isNotEmpty(segment) - ? segment.stream().collect(Collectors.toMap(Segment::getId, it -> it)) - : null; - } - - private static Map mapDataToId(List data) { - return CollectionUtils.isNotEmpty(data) - ? data.stream().collect(Collectors.toMap(Data::getId, it -> it)) - : null; - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java deleted file mode 100644 index a3e8f85c1a1..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMerger.java +++ /dev/null @@ -1,74 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.Uid; -import org.apache.commons.collections4.CollectionUtils; - -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.stream.Collectors; - -public class EidsMerger { - - private EidsMerger() { - } - - public static List merge(List destination, List source) { - if (CollectionUtils.isEmpty(source)) { - return destination; - } - - final Map sourceToEid = mapEidToSource(destination); - - if (sourceToEid == null || sourceToEid.isEmpty()) { - return source; - } - - source.forEach(eid -> sourceToEid.compute(eid.getSource(), (id, item) -> item != null - ? mergeEids(item, eid) - : eid)); - - return sourceToEid.values().stream().toList(); - } - - private static Eid mergeEids(Eid destination, Eid source) { - if (source == null) { - return destination; - } - - final Map idToUid = mapUidToId(destination.getUids()); - - if (idToUid == null || idToUid.isEmpty()) { - return source; - } - - Optional.ofNullable(source.getUids()) - .ifPresent(it -> it.forEach(uid -> idToUid.compute(uid.getId(), (id, item) -> item != null - ? mergeUids(item, uid) - : uid))); - - return destination.toBuilder() - .uids(idToUid.values().stream().toList()) - .build(); - } - - private static Uid mergeUids(Uid destination, Uid source) { - return destination.toBuilder() - .atype(source.getAtype()) - .ext(ExtMerger.mergeExt(destination.getExt(), source.getExt())) - .build(); - } - - private static Map mapEidToSource(List eids) { - return CollectionUtils.isNotEmpty(eids) - ? eids.stream().collect(Collectors.toMap(Eid::getSource, eid -> eid)) - : null; - } - - private static Map mapUidToId(List uids) { - return CollectionUtils.isNotEmpty(uids) - ? uids.stream().collect(Collectors.toMap(Uid::getId, uid -> uid)) - : null; - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index cd049326862..b92ee75cb67 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -33,8 +33,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.Privacy; @@ -57,8 +55,6 @@ public abstract class BaseOptableTest { protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); - protected final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); - protected ModuleContext givenModuleContext() { return givenModuleContext(null); } @@ -88,10 +84,14 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr } protected BidRequest givenBidRequest() { - return givenBidRequest((List) null); + return givenBidRequestWithUserEids((List) null); + } + + protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { + return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build(); } - protected BidRequest givenBidRequest(List eids) { + protected BidRequest givenBidRequestWithUserEids(List eids) { return BidRequest.builder() .user(givenUser(eids)) .device(givenDevice()) @@ -99,8 +99,12 @@ protected BidRequest givenBidRequest(List eids) { .build(); } - protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { - return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build(); + protected BidRequest givenBidRequestWithUserData(List data) { + return BidRequest.builder() + .user(givenUserWithData(data)) + .device(givenDevice()) + .cur(List.of("USD")) + .build(); } protected BidResponse givenBidResponse() { @@ -125,6 +129,10 @@ protected TargetingResult givenTargetingResultWithEids(List eids) { return givenTargetingResult(eids, null); } + protected TargetingResult givenTargetingResultWithData(List data) { + return givenTargetingResult(null, data); + } + protected TargetingResult givenTargetingResult() { return givenTargetingResult(List.of(Eid.builder() .source("source") @@ -179,6 +187,12 @@ protected User givenUser(List eids) { .build(); } + protected User givenUserWithData(List data) { + return User.builder() + .data(data) + .build(); + } + protected Device givenDevice() { return Device.builder().geo(Geo.builder().country("country-d").region("region-d").build()).build(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 5f6cb4f7d3a..9e283f248d8 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -11,8 +11,6 @@ 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.json.JacksonMapper; -import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import java.util.Collection; @@ -37,7 +35,6 @@ public class OptableTargetingModuleTest { UserFpdActivityMask userFpdActivityMask; ObjectMapper mapper = ObjectMapperProvider.mapper(); - JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); @Test public void shouldReturnNonBlankCode() { @@ -60,8 +57,7 @@ public void shouldReturnHooks() { List.of(new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - userFpdActivityMask, - jsonMerger), + userFpdActivityMask), new OptableTargetingAuctionResponseHook( configResolver, mapper)); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index fab9caeaa3b..a26faa7a26c 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -64,8 +64,7 @@ public void setUp() { target = new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, - userFpdActivityMask, - jsonMerger); + userFpdActivityMask); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java index af6b4cb38c0..658f8c6214e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -1,5 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; @@ -7,6 +13,9 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import java.util.Collections; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; public class BidRequestEnricherTest extends BaseOptableTest { @@ -17,7 +26,8 @@ public void shouldReturnOriginBidRequestWhenNoTargetingResults() { final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); // when - final AuctionRequestPayload result = BidRequestEnricher.of(null).apply(auctionRequestPayload); + final AuctionRequestPayload result = BidRequestEnricher.of(null) + .apply(auctionRequestPayload); // then assertThat(result).isNotNull(); @@ -34,7 +44,8 @@ public void shouldNotFailIfBidRequestIsNull() { final TargetingResult targetingResult = givenTargetingResult(); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult).apply(auctionRequestPayload); + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); // then assertThat(result.bidRequest()).isNull(); @@ -47,7 +58,8 @@ public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { final TargetingResult targetingResult = givenTargetingResult(); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult).apply(auctionRequestPayload); + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); // then assertThat(result.bidRequest()).isNotNull(); @@ -57,6 +69,264 @@ public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); } + @Test + public void shouldNotAddEidWhenSourceAlreadyPresent() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("source", List.of(givenUid("id", null, null)), null), + givenEid("source1", List.of(givenUid("id", null, null)), null) + )); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source", List.of(givenUid("id2", 3, null)), null) + )); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids).filteredOn(it -> it.getSource().equals("source")).hasSize(1); + } + + @Test + public void shouldAddEidWhenSourceIsNotAlreadyPresent() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("source1", List.of(givenUid("id", null, null)), null), + givenEid("source2", List.of(givenUid("id", null, null)), null) + )); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source3", List.of(givenUid("id2", 3, null)), null) + )); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(3); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2", "source3"); + } + + @Test + public void shouldNotMergeOriginEidsWithTheSameSource() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("source", List.of(givenUid("id", null, null)), null), + givenEid("source", List.of(givenUid("id", null, null)), null) + )); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source3", List.of(givenUid("id2", 3, null)), null) + )); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(3); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source", "source", "source3"); + } + + @Test + public void shouldApplyOriginEidsWhenTargetingIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source3", List.of(givenUid("id2", 3, null)), null) + )); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(1); + assertThat(eids).extracting(Eid::getSource).containsExactly("source3"); + } + + @Test + public void shouldApplyTargetingEidsWhenOriginListIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("source", List.of(givenUid("id", null, null)), null), + givenEid("source1", List.of(givenUid("id", null, null)), null) + )); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids).extracting(Eid::getSource).containsExactly("source", "source1"); + } + + @Test + public void shouldNotApplyEidsWhenOriginAndTargetingEidsAreEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserEids(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids).isEmpty(); + } + + @Test + public void shouldMergeDataWithTheSameId() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id", List.of(givenSegment("id3", "value3"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(2); + assertThat(data).extracting(Data::getId).containsExactly("id", "id"); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1", "id3"); + assertThat(segments.getLast()).extracting(Segment::getId).containsExactly("id2", "id3"); + }); + } + + @Test + public void shouldMergeDistinctSegmentsWithinTheSameData() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id1", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id", List.of(givenSegment("id1", "value3"))), + givenData("id", List.of(givenSegment("id4", "value4"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(2); + assertThat(data).extracting(Data::getId).containsExactly("id", "id1"); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1", "id4"); + assertThat(segments.getFirst()).filteredOn(it -> it.getId().equals("id1")) + .extracting(Segment::getValue).containsExactly("value1"); + assertThat(segments.getLast()).extracting(Segment::getId).containsExactly("id2"); + }); + } + + @Test + public void shouldAppendDataWithNewId() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id1", List.of(givenSegment("id3", "value3"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(3); + assertThat(data).extracting(Data::getId).containsExactly("id", "id", "id1"); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1"); + assertThat(segments.get(1)).extracting(Segment::getId).containsExactly("id2"); + assertThat(segments.getLast()).extracting(Segment::getId).containsExactly("id3"); + }); + } + + @Test + public void shouldApplyOriginDataWhenTargetingIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))), + givenData("id", List.of(givenSegment("id2", "value2"))))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(2); + assertThat(data).extracting(Data::getSegment).satisfies(segments -> { + assertThat(segments.getFirst()).extracting(Segment::getId).containsExactly("id1"); + assertThat(segments.get(1)).extracting(Segment::getId).containsExactly("id2"); + }); + } + + @Test + public void shouldApplyTargetingDataWhenOriginIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(List.of( + givenData("id", List.of(givenSegment("id1", "value1"))))); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data.size()).isEqualTo(1); + assertThat(data).flatMap(Data::getSegment).extracting(Segment::getId).containsExactly("id1"); + } + + @Test + public void shouldApplyNothingWhenOriginAndTargetingDataAreEmpty() { + // given + final BidRequest bidRequest = givenBidRequestWithUserData(Collections.emptyList()); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final TargetingResult targetingResult = givenTargetingResultWithData(Collections.emptyList()); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); + + // then + final List data = result.bidRequest().getUser().getData(); + assertThat(data).isEmpty(); + } + @Test public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { // given @@ -64,7 +334,8 @@ public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { final TargetingResult targetingResult = givenEmptyTargetingResult(); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult).apply(auctionRequestPayload); + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + .apply(auctionRequestPayload); // then assertThat(result.bidRequest()).isNotNull(); @@ -73,4 +344,34 @@ public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { assertThat(user.getEids()).isNull(); assertThat(user.getData()).isNull(); } + + private Eid givenEid(String source, List uids, ObjectNode ext) { + return Eid.builder() + .source(source) + .uids(uids) + .ext(ext) + .build(); + } + + private Uid givenUid(String id, Integer atype, ObjectNode ext) { + return Uid.builder() + .id(id) + .atype(atype) + .ext(ext) + .build(); + } + + private Data givenData(String id, List segments) { + return Data.builder() + .id(id) + .segment(segments) + .build(); + } + + private Segment givenSegment(String id, String value) { + return Segment.builder() + .id(id) + .value(value) + .build(); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java deleted file mode 100644 index e7df549391b..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/BaseMergerTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.util.Map; - -public class BaseMergerTest { - - protected final ObjectMapper mapper = new ObjectMapper(); - - protected ObjectNode givenExt(Map fields) { - final ObjectNode ext = mapper.createObjectNode(); - fields.forEach(ext::put); - - return ext; - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java deleted file mode 100644 index e9bdeeb80c3..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/DataMergerTest.java +++ /dev/null @@ -1,148 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.iab.openrtb.request.Data; -import com.iab.openrtb.request.Segment; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.from; - -public class DataMergerTest extends BaseMergerTest { - - @Test - public void shouldMergeDifferentData() { - // given - final List destination = givenORTBData("dataId1", "segmentId1", - "field1", "value1"); - final List source = givenOptableData("dataId2", "segmentId2", "field2", "value2"); - - // when - final List result = DataMerger.merge(destination, source); - - // then - assertThat(result).isNotNull() - .hasSize(2); - - assertThat(result.getFirst()) - .returns("dataId2", from(Data::getId)) - .returns("segmentId2", it -> it.getSegment().getFirst().getId()) - .returns("value2", it -> it.getSegment().getFirst().getExt().get("field2").asText()); - - assertThat(result.get(1)) - .returns("dataId1", from(Data::getId)) - .returns("segmentId1", it -> it.getSegment().getFirst().getId()) - .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); - } - - @Test - public void shouldMergeSegmentsWithinTheSameData() { - // given - final List destination = givenORTBData("dataId1", "segmentId1", - "field1", "value1"); - final List source = givenOptableData("dataId1", "segmentId2", "field2", "value2"); - - // when - final List result = DataMerger.merge(destination, source); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("dataId1", from(Data::getId)) - .returns("segmentId1", it -> it.getSegment().getFirst().getId()) - .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()) - .returns("segmentId2", it -> it.getSegment().get(1).getId()) - .returns("value2", it -> it.getSegment().get(1).getExt().get("field2").asText()); - } - - @Test - public void shouldMergeExtWithinTheSameSegment() { - // given - final List destination = givenORTBData("dataId1", "segmentId1", - "field1", "value1"); - final List source = givenOptableData("dataId1", "segmentId1", "field2", "value2"); - - // when - final List result = DataMerger.merge(destination, source); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("dataId1", from(Data::getId)) - .returns("segmentId1", it -> it.getSegment().getFirst().getId()) - .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()) - .returns("value2", it -> it.getSegment().getFirst().getExt().get("field2").asText()); - } - - @Test - public void shouldUseFirstArgumentWhenSecondIsAbsent() { - // given - final List source = givenOptableData("dataId1", "segmentId1", "field1", "value1"); - - // when - final List result = DataMerger.merge(null, source); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("dataId1", from(Data::getId)) - .returns("segmentId1", it -> it.getSegment().getFirst().getId()) - .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); - } - - @Test - public void shouldUseSecondArgumentWhenFirstIsAbsent() { - // given - final List destination = givenORTBData("dataId1", "segmentId1", - "field1", "value1"); - // when - final List result = DataMerger.merge(destination, null); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("dataId1", from(Data::getId)) - .returns("segmentId1", it -> it.getSegment().getFirst().getId()) - .returns("value1", it -> it.getSegment().getFirst().getExt().get("field1").asText()); - } - - @Test - public void shouldNotFailWhenArgumentsAreAbsent() { - // given and when - final List result = DataMerger.merge(null, null); - - // then - assertThat(result).isNull(); - } - - private List givenOptableData(String id, String segmentId, String extField, String extValue) { - return List.of(Data.builder() - .id(id) - .segment(List.of(Segment.builder() - .id(segmentId) - .ext(givenExt(Map.of(extField, extValue))) - .build())) - .build()); - } - - private List givenORTBData(String id, String segmentId, String extField, String extValue) { - - return List.of(Data.builder() - .id(id) - .segment(List.of(Segment.builder() - .id(segmentId) - .ext(givenExt(Map.of(extField, extValue))) - .build())) - .build()); - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java deleted file mode 100644 index ca39ad458da..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/EidsMergerTest.java +++ /dev/null @@ -1,125 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.Uid; -import org.junit.jupiter.api.Test; - -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.from; - -public class EidsMergerTest extends BaseMergerTest { - - @Test - public void shouldMergeDifferentEids() { - // given - final List destination = givenEids("source1", "uid1", "field1", "value1"); - final List source = givenEids("source2", "uid2", "field2", "value2"); - - // when - final List result = EidsMerger.merge(destination, source); - - // then - assertThat(result).isNotNull() - .hasSize(2); - - assertThat(result.getFirst()) - .returns("source1", from(Eid::getSource)) - .returns("uid1", it -> it.getUids().getFirst().getId()) - .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); - - assertThat(result.get(1)) - .returns("source2", from(Eid::getSource)) - .returns("uid2", it -> it.getUids().getFirst().getId()) - .returns("value2", it -> it.getUids().getFirst().getExt().get("field2").asText()); - } - - @Test - public void shouldMergeUidsWithinTheSameEid() { - // given - final List destination = givenEids("source1", "uid1", "field1", "value1"); - final List source = givenEids("source1", "uid2", "field2", "value2"); - - // when - final List result = EidsMerger.merge(destination, source); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("source1", from(Eid::getSource)) - .returns("uid2", it -> it.getUids().getFirst().getId()) - .returns("uid1", it -> it.getUids().get(1).getId()) - .returns("value2", it -> it.getUids().getFirst().getExt().get("field2").asText()) - .returns("value1", it -> it.getUids().get(1).getExt().get("field1").asText()); - } - - @Test - public void shouldMergeExtWithinTheSameUid() { - // given - final List destination = givenEids("source1", "uid1", "field1", "value1"); - final List source = givenEids("source1", "uid1", "field2", "value2"); - - // when - final List result = EidsMerger.merge(destination, source); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("source1", from(Eid::getSource)) - .returns("uid1", it -> it.getUids().getFirst().getId()) - .returns("value2", it -> it.getUids().getFirst().getExt().get("field2").asText()) - .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); - } - - @Test - public void shouldUserFirstUidsListWhenSecondIsAbsent() { - // given - final List destination = givenEids("source1", "uid1", "field1", "value1"); - - // when - final List result = EidsMerger.merge(destination, null); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("source1", from(Eid::getSource)) - .returns("uid1", it -> it.getUids().getFirst().getId()) - .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); - } - - @Test - public void shouldUserSecondUidsListWhenSecondIsEmpty() { - // given - final List source = givenEids("source1", "uid1", "field1", "value1"); - - // when - final List result = EidsMerger.merge(null, source); - - // then - assertThat(result).isNotNull() - .hasSize(1); - - assertThat(result.getFirst()) - .returns("source1", from(Eid::getSource)) - .returns("uid1", it -> it.getUids().getFirst().getId()) - .returns("value1", it -> it.getUids().getFirst().getExt().get("field1").asText()); - } - - private List givenEids(String source, String uidId, String extField, String extValue) { - return List.of(Eid.builder() - .source(source) - .uids(List.of(Uid.builder() - .id(uidId) - .ext(givenExt(Map.of(extField, extValue))) - .build())) - .build()); - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java index 2d01c66d14f..e95b39d47f9 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import org.junit.jupiter.api.Test; @@ -7,7 +8,9 @@ import static org.assertj.core.api.Assertions.assertThat; -public class ExtMergerTest extends BaseMergerTest { +public class ExtMergerTest { + + protected final ObjectMapper mapper = new ObjectMapper(); @Test public void shouldMergeTwoExtObjects() { @@ -62,4 +65,11 @@ public void shouldNotFail() { // then assertThat(result).isNull(); } + + protected ObjectNode givenExt(Map fields) { + final ObjectNode ext = mapper.createObjectNode(); + fields.forEach(ext::put); + + return ext; + } } From ee51258ee99f1682952516ccbc5a6076cbb7795d Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Fri, 4 Jul 2025 10:00:17 +0200 Subject: [PATCH 35/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 73 +++------ .../targeting/model/OptableAttributes.java | 4 - .../targeting/model/net/OptableCall.java | 9 +- .../OptableTargetingAuctionResponseHook.java | 11 +- ...eTargetingProcessedAuctionRequestHook.java | 7 +- .../targeting/v1/core/BidRequestEnricher.java | 42 ++--- .../v1/core/BidResponseEnricher.java | 111 +++++++------ .../optable/targeting/v1/core/Cache.java | 14 +- .../v1/core/OptableAttributesResolver.java | 17 +- .../targeting/v1/core/OptableTargeting.java | 39 ++--- .../targeting/v1/core/QueryBuilder.java | 95 ++++++----- .../targeting/v1/core/merger/ExtMerger.java | 23 --- .../optable/targeting/v1/net/APIClient.java | 3 +- .../targeting/v1/net/APIClientFactory.java | 21 --- .../targeting/v1/net/APIClientImpl.java | 148 ++++++------------ .../targeting/v1/net/CachedAPIClient.java | 50 +++--- .../v1/net/OptableResponseMapper.java | 31 ---- .../optable/targeting/v1/BaseOptableTest.java | 4 + ...tableTargetingAuctionResponseHookTest.java | 6 +- .../v1/OptableTargetingModuleTest.java | 9 +- ...getingProcessedAuctionRequestHookTest.java | 9 +- .../v1/core/BidResponseEnricherTest.java | 20 +-- .../optable/targeting/v1/core/CacheTest.java | 12 +- .../core/OptableAttributesResolverTest.java | 11 +- .../v1/core/OptableTargetingTest.java | 31 ++-- .../targeting/v1/core/QueryBuilderTest.java | 23 ++- .../v1/core/merger/ExtMergerTest.java | 75 --------- .../targeting/v1/net/APIClientTest.java | 67 ++++---- .../targeting/v1/net/CachedAPIClientTest.java | 29 ++-- .../v1/net/OptableResponseMapperTest.java | 96 ------------ 30 files changed, 361 insertions(+), 729 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 06f77faab00..7a3faca8427 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.config; import io.vertx.core.Vertx; +import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.cache.PbcStorageService; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; @@ -11,19 +12,15 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.QueryBuilder; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientFactory; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.spring.config.VertxContextScope; import org.prebid.server.spring.config.model.HttpClientProperties; -import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -49,16 +46,6 @@ IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") doubl return new IdsMapper(ObjectMapperProvider.mapper(), logSamplingRate); } - @Bean - QueryBuilder queryBuilder() { - return new QueryBuilder(); - } - - @Bean - OptableResponseMapper optableResponseParser(JacksonMapper mapper) { - return new OptableResponseMapper(mapper); - } - @Bean @Scope(scopeName = VertxContextScope.NAME, proxyMode = ScopedProxyMode.TARGET_CLASS) @ConditionalOnProperty(prefix = "http-client.circuit-breaker", name = "enabled", havingValue = "false", @@ -67,56 +54,40 @@ OptableHttpClientWrapper optableHttpClient(Vertx vertx, HttpClientProperties htt return new OptableHttpClientWrapper(vertx, httpClientProperties); } - @Bean(name = "apiClient") - APIClient apiClientImpl(OptableHttpClientWrapper httpClientWrapper, - @Value("${logging.sampling-rate:0.01}") - double logSamplingRate, - OptableTargetingProperties properties, - OptableResponseMapper responseParser) { + @Bean + APIClientImpl apiClient(OptableHttpClientWrapper httpClientWrapper, + @Value("${logging.sampling-rate:0.01}") + double logSamplingRate, + OptableTargetingProperties properties, + JacksonMapper jacksonMapperr) { return new APIClientImpl( properties.getApiEndpoint(), httpClientWrapper.getHttpClient(), - logSamplingRate, - responseParser); - } - - @Bean(name = "cachedAPIClient") - APIClient cachedApiClient(APIClientImpl apiClient, Cache cache) { - return new CachedAPIClient(apiClient, cache); + jacksonMapperr, + logSamplingRate); } @Bean - APIClientFactory apiClientFactory( - @Qualifier("apiClient") - APIClient apiClient, - @Qualifier("cachedAPIClient") - APIClient cachedApiClient, - @Value("${storage.pbc.enabled:false}") - boolean storageEnabled, - @Value("${cache.module.enabled:false}") - boolean moduleCacheEnabled) { - - return new APIClientFactory( - apiClient, - cachedApiClient, - storageEnabled && moduleCacheEnabled); + @ConditionalOnProperty(name = {"storage.pbc.enabled", "cache.module.enabled"}, havingValue = "true") + CachedAPIClient cachedApiClient(APIClientImpl apiClient, Cache cache) { + return new CachedAPIClient(apiClient, cache); } @Bean - Cache cache(PbcStorageService cacheService, OptableResponseMapper responseMapper) { - return new Cache(cacheService, responseMapper); + @ConditionalOnProperty(name = {"storage.pbc.enabled", "cache.module.enabled"}, havingValue = "true") + Cache cache(PbcStorageService cacheService, JacksonMapper jacksonMapper) { + return new Cache(cacheService, jacksonMapper); } @Bean OptableTargeting optableTargeting(IdsMapper parametersExtractor, - QueryBuilder queryBuilder, - APIClientFactory apiClientFactory) { + APIClientImpl apiClient, + @Autowired(required = false) CachedAPIClient cachedApiClient) { return new OptableTargeting( parametersExtractor, - queryBuilder, - apiClientFactory); + ObjectUtils.firstNonNull(cachedApiClient, apiClient)); } @Bean @@ -127,7 +98,8 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask) { + UserFpdActivityMask userFpdActivityMask, + JsonMerger jsonMerger) { return new OptableTargetingModule(List.of( new OptableTargetingProcessedAuctionRequestHook( @@ -136,6 +108,7 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, userFpdActivityMask), new OptableTargetingAuctionResponseHook( configResolver, - ObjectMapperProvider.mapper()))); + ObjectMapperProvider.mapper(), + jsonMerger))); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index 2ef95eb429d..5498457dcbb 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -21,8 +21,4 @@ public class OptableAttributes { List ips; Long timeout; - - public static OptableAttributes empty() { - return OptableAttributes.builder().build(); - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java index 0f41164680c..45f242bc983 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java @@ -1,18 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.model.net; -import lombok.AccessLevel; -import lombok.AllArgsConstructor; import lombok.Value; -@Value -@AllArgsConstructor(access = AccessLevel.PRIVATE) +@Value(staticConstructor = "succeededHttp") public class OptableCall { HttpRequest request; HttpResponse response; - - public static OptableCall succeededHttp(HttpRequest request, HttpResponse response) { - return new OptableCall(request, response); - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 321930541e5..2139c5ccacc 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -20,6 +20,7 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JsonMerger; import java.util.List; import java.util.Objects; @@ -31,9 +32,15 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook private final ConfigResolver configResolver; private final ObjectMapper objectMapper; - public OptableTargetingAuctionResponseHook(ConfigResolver configResolver, ObjectMapper objectMapper) { + private final JsonMerger jsonMerger; + + public OptableTargetingAuctionResponseHook(ConfigResolver configResolver, + ObjectMapper objectMapper, + JsonMerger jsonMerger) { + this.configResolver = Objects.requireNonNull(configResolver); this.objectMapper = Objects.requireNonNull(objectMapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); } @Override @@ -63,7 +70,7 @@ private Future> enrichedPayload(ModuleC final List targeting = moduleContext.getTargeting(); return CollectionUtils.isNotEmpty(targeting) - ? update(BidResponseEnricher.of(objectMapper, targeting), moduleContext) + ? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext) : success(moduleContext); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 19a0e0e06d5..572133a177d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -12,6 +12,7 @@ import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; @@ -60,7 +61,7 @@ public Future> call(AuctionRequestPayloa final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); - final long timeout = getHookRemainingTime(invocationContext); + final Timeout timeout = getHookTimeout(invocationContext); final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), properties.getTimeout()); @@ -115,8 +116,8 @@ private BidRequest maskUserPersonalInfo(BidRequest bidRequest, .build(); } - private long getHookRemainingTime(AuctionInvocationContext invocationContext) { - return invocationContext.timeout().remaining(); + private Timeout getHookTimeout(AuctionInvocationContext invocationContext) { + return invocationContext.timeout(); } private Future> enrichedPayload(TargetingResult targetingResult, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index c142e5a3053..d7fa0335b4e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -12,8 +12,7 @@ import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import java.util.HashSet; -import java.util.LinkedHashMap; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; @@ -75,7 +74,7 @@ private List mergeEids(List destination, List source) { Eid::getSource); } - private List mergeData(List destination, List source) { + private static List mergeData(List destination, List source) { if (CollectionUtils.isEmpty(destination)) { return source; } @@ -85,34 +84,23 @@ private List mergeData(List destination, List source) { } final Map idToSourceData = source.stream() - .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, LinkedHashMap::new)); + .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new)); - final Set mergedDataIds = new HashSet<>(); - - final Stream mergedData = destination.stream() - .map(destinationData -> mergeDataEntry(destinationData, idToSourceData, mergedDataIds)); + final List mergedData = destination.stream() + .map(destinationData -> idToSourceData.containsKey(destinationData.getId()) + ? mergeData(destinationData, idToSourceData.get(destinationData.getId())) + : destinationData) + .toList(); - return Stream.concat(mergedData, source.stream().filter(it -> !mergedDataIds.contains(it.getId()))).toList(); + return merge(mergedData, source, Data::getId); } - private Data mergeData(Data destinationData, Data sourceData) { - return Data.builder() - .id(destinationData.getId()) - .name(destinationData.getName()) - .ext(destinationData.getExt()) + private static Data mergeData(Data destinationData, Data sourceData) { + return destinationData.toBuilder() .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId)) .build(); } - private Data mergeDataEntry(Data destinationData, Map idToSourceData, Set mergedDataIds) { - return Optional.ofNullable(idToSourceData.get(destinationData.getId())) - .map(sourceData -> { - mergedDataIds.add(sourceData.getId()); - return mergeData(destinationData, sourceData); - }) - .orElse(destinationData); - } - private static List merge(List destination, List source, Function idExtractor) { @@ -129,10 +117,10 @@ private static List merge(List destination, .map(idExtractor) .collect(Collectors.toSet()); - final List uniqueFromSource = source.stream() - .filter(entry -> !existingIds.contains(idExtractor.apply(entry))) + return Stream.concat( + destination.stream(), + source.stream() + .filter(entry -> !existingIds.contains(idExtractor.apply(entry)))) .toList(); - - return Stream.concat(destination.stream(), uniqueFromSource.stream()).toList(); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java index 48cf2f6ebbf..ec9fe4ef0a7 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricher.java @@ -7,89 +7,104 @@ import com.iab.openrtb.response.Bid; import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; -import lombok.Value; -import lombok.experimental.Accessors; import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.merger.ExtMerger; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JsonMerger; -import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; -@Accessors(fluent = true) -@Value(staticConstructor = "of") public class BidResponseEnricher implements PayloadUpdate { - ObjectMapper mapper; - List targeting; + private final List targeting; + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + + private BidResponseEnricher(List targeting, ObjectMapper mapper, JsonMerger jsonMerger) { + this.targeting = targeting; + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public static BidResponseEnricher of(List targeting, ObjectMapper mapper, JsonMerger jsonMerger) { + return new BidResponseEnricher(targeting, mapper, jsonMerger); + } @Override public AuctionResponsePayload apply(AuctionResponsePayload payload) { - return AuctionResponsePayloadImpl.of(enrichBidResponse(mapper, payload.bidResponse(), targeting)); + return AuctionResponsePayloadImpl.of(enrichBidResponse(payload.bidResponse(), targeting)); } - private static BidResponse enrichBidResponse(ObjectMapper mapper, - BidResponse bidResponse, - List targeting) { - - if (bidResponse == null || CollectionUtils.isEmpty(targeting)) { + private BidResponse enrichBidResponse(BidResponse bidResponse, List targeting) { + if (CollectionUtils.isEmpty(targeting)) { return bidResponse; } - final ObjectNode node = targetingToObjectNode(mapper, targeting); - if (node == null) { + final ObjectNode node = targetingToObjectNode(targeting); + if (node.isEmpty()) { return bidResponse; } - final List seatBids = Optional.ofNullable(bidResponse.getSeatbid()) - .orElse(Collections.emptyList()) - .stream().map(seatBid -> { - final List bids = Optional.ofNullable(seatBid.getBid()) - .orElse(Collections.emptyList()) - .stream() - .map(bid -> applyTargeting(mapper, bid, node)) - .toList(); - - return seatBid.toBuilder().bid(bids).build(); - }).toList(); + final List seatBids = CollectionUtils.emptyIfNull(bidResponse.getSeatbid()).stream() + .map(seatBid -> seatBid.toBuilder() + .bid(CollectionUtils.emptyIfNull(seatBid.getBid()).stream() + .map(bid -> applyTargeting(bid, node)) + .toList()) + .build()) + .toList(); return bidResponse.toBuilder() .seatbid(seatBids) .build(); } - private static Bid applyTargeting(ObjectMapper mapper, Bid bid, ObjectNode node) { - final ObjectNode extNode = getOrCreateNode(mapper, bid.getExt()); - final ObjectNode prebidNode = getOrCreateNode(mapper, (ObjectNode) extNode.get("prebid")); - final ObjectNode targetingNode = getOrCreateNode(mapper, (ObjectNode) prebidNode.get("targeting")); - final JsonNode mergedTargetingNode = ExtMerger.mergeExt(targetingNode, node); - - prebidNode.set("targeting", mergedTargetingNode); - extNode.set("prebid", prebidNode); - - return bid.toBuilder().ext(extNode).build(); - } - - private static ObjectNode getOrCreateNode(ObjectMapper mapper, ObjectNode node) { - return Optional.ofNullable(node).orElseGet(mapper::createObjectNode); - } - - private static ObjectNode targetingToObjectNode(ObjectMapper mapper, List targeting) { + private ObjectNode targetingToObjectNode(List targeting) { final ObjectNode node = mapper.createObjectNode(); - for (Audience audience: targeting) { + for (Audience audience : targeting) { final List ids = audience.getIds(); - if (CollectionUtils.isNotEmpty(ids)) { - final List strIds = ids.stream().map(AudienceId::getId).toList(); - node.putIfAbsent(audience.getKeyspace(), TextNode.valueOf(String.join(",", strIds))); + if (CollectionUtils.isEmpty(ids)) { + continue; } + + final String joinedIds = ids.stream() + .map(AudienceId::getId) + .collect(Collectors.joining(",")); + node.putIfAbsent(audience.getKeyspace(), TextNode.valueOf(joinedIds)); } return node; } + + private Bid applyTargeting(Bid bid, ObjectNode node) { + final ObjectNode ext = Optional.ofNullable(bid.getExt()) + .map(ObjectNode::deepCopy) + .orElseGet(mapper::createObjectNode); + + final ObjectNode prebid = newNodeIfNull(ext.get("prebid")); + final ObjectNode targeting; + try { + targeting = (ObjectNode) jsonMerger.merge(node, newNodeIfNull(prebid.get("targeting"))); + } catch (InvalidRequestException e) { + return bid; + } + + prebid.set("targeting", targeting); + ext.set("prebid", prebid); + + return bid.toBuilder().ext(ext).build(); + } + + private ObjectNode newNodeIfNull(JsonNode node) { + return node == null || !node.isObject() + ? mapper.createObjectNode() + : (ObjectNode) node; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java index 9e03fdbe24f..6aab8698a4a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Cache.java @@ -5,7 +5,7 @@ import org.prebid.server.cache.proto.request.module.StorageDataType; import org.prebid.server.cache.proto.response.module.ModuleCacheResponse; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; +import org.prebid.server.json.JacksonMapper; import java.util.Objects; @@ -15,19 +15,17 @@ public class Cache { private static final String APPLICATION = "optable-targeting"; private final PbcStorageService cacheService; - private final OptableResponseMapper optableResponseMapper; - - public Cache(PbcStorageService cacheService, - OptableResponseMapper optableResponseMapper) { + private final JacksonMapper mapper; + public Cache(PbcStorageService cacheService, JacksonMapper mapper) { this.cacheService = Objects.requireNonNull(cacheService); - this.optableResponseMapper = Objects.requireNonNull(optableResponseMapper); + this.mapper = Objects.requireNonNull(mapper); } public Future get(String query) { return cacheService.retrieveEntry(query, APP_CODE, APPLICATION) .map(ModuleCacheResponse::getValue) - .map(optableResponseMapper::parse); + .map(body -> body != null ? mapper.decodeValue(body, TargetingResult.class) : null); } public Future put(String query, TargetingResult value, int ttlSeconds) { @@ -37,7 +35,7 @@ public Future put(String query, TargetingResult value, int ttlSeconds) { return cacheService.storeEntry( query, - optableResponseMapper.toJsonString(value), + mapper.encodeToString(value), StorageDataType.TEXT, ttlSeconds, APPLICATION, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 3ac6881acee..4b635b6c84e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,14 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.collections4.SetUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.privacy.gdpr.model.TcfContext; -import org.prebid.server.privacy.model.PrivacyContext; import java.util.ArrayList; import java.util.List; @@ -45,25 +42,15 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, public static List resolveIp(AuctionContext auctionContext) { final List result = new ArrayList<>(); - final Optional auctionContextOpt = Optional.ofNullable(auctionContext); - - final Optional deviceOpt = auctionContextOpt - .map(AuctionContext::getBidRequest) - .map(BidRequest::getDevice); - + final Optional deviceOpt = Optional.ofNullable(auctionContext.getBidRequest().getDevice()); deviceOpt.map(Device::getIp).ifPresent(result::add); deviceOpt.map(Device::getIpv6).ifPresent(result::add); if (result.isEmpty()) { - auctionContextOpt.map(AuctionContext::getPrivacyContext) - .map(PrivacyContext::getIpAddress) + Optional.ofNullable(auctionContext.getPrivacyContext().getTcfContext().getIpAddress()) .ifPresent(result::add); } return result; } - - public static String resolveIp(List ips) { - return CollectionUtils.isNotEmpty(ips) ? ips.getFirst() : "none"; - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index b6d84f3a67d..c45ce8b6f9f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -2,43 +2,38 @@ import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientFactory; +import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClient; +import java.util.List; import java.util.Objects; -import java.util.Optional; public class OptableTargeting { private final IdsMapper idsMapper; - private final QueryBuilder queryBuilder; - private final APIClientFactory apiClientFactory; - - public OptableTargeting(IdsMapper idsMapper, - QueryBuilder queryBuilder, - APIClientFactory apiClientFactory) { + private final APIClient apiClient; + public OptableTargeting(IdsMapper idsMapper, APIClient apiClient) { this.idsMapper = Objects.requireNonNull(idsMapper); - this.queryBuilder = Objects.requireNonNull(queryBuilder); - this.apiClientFactory = Objects.requireNonNull(apiClientFactory); + this.apiClient = Objects.requireNonNull(apiClient); } public Future getTargeting(OptableTargetingProperties properties, BidRequest bidRequest, OptableAttributes attributes, - long timeout) { - - return Optional.ofNullable(bidRequest) - .map(it -> idsMapper.toIds(it, properties.getPpidMapping())) - .map(ids -> queryBuilder.build(ids, attributes, properties.getIdPrefixOrder())) - .map(query -> - apiClientFactory.getClient(properties.getCache().isEnabled()).getTargeting( - properties, - query, - attributes.getIps(), - timeout)) - .orElse(Future.failedFuture("Can't get targeting")); + Timeout timeout) { + + final List ids = idsMapper.toIds(bidRequest, properties.getPpidMapping()); + final Query query = QueryBuilder.build(ids, attributes, properties.getIdPrefixOrder()); + if (query == null) { + return Future.failedFuture("Can't get targeting"); + } + + return apiClient.getTargeting(properties, query, attributes.getIps(), timeout); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index c0bd355a26b..2cf964bf63b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -9,14 +9,22 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.List; +import java.util.Map; import java.util.Optional; -import java.util.stream.Stream; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.IntStream; public class QueryBuilder { - public Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { + private QueryBuilder() { + } + + public static Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { if (CollectionUtils.isEmpty(ids) && CollectionUtils.isEmpty(optableAttributes.getIps())) { return null; } @@ -24,66 +32,55 @@ public Query build(List ids, OptableAttributes optableAttributes, String idP return Query.of(buildIdsString(ids, idPrefixOrder), buildAttributesString(optableAttributes)); } - private String buildIdsString(List ids, String idPrefixOrder) { - final StringBuilder sb = new StringBuilder(); + private static String buildIdsString(List ids, String idPrefixOrder) { + if (CollectionUtils.isEmpty(ids)) { + return StringUtils.EMPTY; + } + final List reorderedIds = reorderIds(ids, idPrefixOrder); - if (CollectionUtils.isNotEmpty(reorderedIds)) { - buildQueryString(sb, reorderedIds); + + final StringBuilder sb = new StringBuilder(); + for (Id id : reorderedIds) { + sb.append("&id="); + sb.append(URLEncoder.encode( + "%s:%s".formatted(id.getName(), id.getValue()), + StandardCharsets.UTF_8)); } return sb.toString(); } - private List reorderIds(List ids, String idPrefixOrder) { - if (!StringUtils.isEmpty(idPrefixOrder)) { - final int lastIndex = ids.size() - 1; - final List order = Stream.of(idPrefixOrder.split(",", -1)).toList(); - final List orderedIds = new ArrayList<>(ids); + private static List reorderIds(List ids, String idPrefixOrder) { + if (StringUtils.isEmpty(idPrefixOrder)) { + return ids; + } - orderedIds.sort(Comparator.comparing(item -> checkOrder(item, order, lastIndex))); + final String[] prefixOrder = idPrefixOrder.split(","); + final Map prefixToPriority = IntStream.range(0, prefixOrder.length).boxed() + .collect(Collectors.toMap(i -> prefixOrder[i], Function.identity())); - return orderedIds; - } - return ids; - } + final List orderedIds = new ArrayList<>(ids); + orderedIds.sort(Comparator.comparing(item -> prefixToPriority.getOrDefault(item.getName(), Integer.MAX_VALUE))); - private int checkOrder(Id item, List order, int lastIndex) { - int value = order.indexOf(item.getName()); - if (value == -1) { - value = lastIndex; - } - return value; + return orderedIds; } - private String buildAttributesString(OptableAttributes optableAttributes) { + private static String buildAttributesString(OptableAttributes optableAttributes) { final StringBuilder sb = new StringBuilder(); - Optional.ofNullable(optableAttributes.getGdprConsent()).ifPresent(consent -> - sb.append("&gdpr_consent=").append(consent)); - Optional.of(optableAttributes.isGdprApplies()).ifPresent(applies -> - sb.append("&gdpr=").append(applies ? 1 : 0)); - Optional.ofNullable(optableAttributes.getGpp()).ifPresent(tcf -> - sb.append("&gpp=").append(tcf)); - Optional.ofNullable(optableAttributes.getGppSid()).ifPresent(gppSids -> { - if (CollectionUtils.isNotEmpty(gppSids)) { - sb.append("&gpp_sid=").append(gppSids.stream().findFirst()); - } - }); - Optional.ofNullable(optableAttributes.getTimeout()).ifPresent(timeout -> - sb.append("&timeout=").append(timeout).append("ms")); - return sb.toString(); - } + Optional.ofNullable(optableAttributes.getGdprConsent()) + .ifPresent(consent -> sb.append("&gdpr_consent=").append(consent)); + sb.append("&gdpr=").append(optableAttributes.isGdprApplies() ? 1 : 0); - private void buildQueryString(StringBuilder sb, List ids) { - final int size = ids.size(); - for (int index = 0; index < size; index++) { - final Id id = ids.get(index); - sb.append(URLEncoder.encode( - "%s:%s".formatted(id.getName(), id.getValue()), - StandardCharsets.UTF_8)); - if (index != size - 1) { - sb.append("&id="); - } - } + Optional.ofNullable(optableAttributes.getGpp()) + .ifPresent(gpp -> sb.append("&gpp=").append(gpp)); + Optional.ofNullable(optableAttributes.getGppSid()) + .filter(Predicate.not(Collection::isEmpty)) + .ifPresent(gppSids -> sb.append("&gpp_sid=").append(gppSids.stream().findFirst())); + + Optional.ofNullable(optableAttributes.getTimeout()) + .ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms")); + + return sb.toString(); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java deleted file mode 100644 index a8977d45739..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMerger.java +++ /dev/null @@ -1,23 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.fasterxml.jackson.databind.node.ObjectNode; - -import java.util.Optional; - -public class ExtMerger { - - private ExtMerger() { - } - - public static ObjectNode mergeExt(ObjectNode origin, ObjectNode newExt) { - if (newExt == null) { - return origin; - } - - return Optional.ofNullable(origin) - .map(it -> { - newExt.fieldNames().forEachRemaining(field -> it.set(field, newExt.get(field))); - return it; - }).orElse(newExt); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index 42dccf9208e..9e0a4f3db17 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; @@ -12,5 +13,5 @@ public interface APIClient { Future getTargeting(OptableTargetingProperties properties, Query query, List ips, - long timeout); + Timeout timeout); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java deleted file mode 100644 index 28e178323ae..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientFactory.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.net; - -import java.util.Objects; - -public class APIClientFactory { - - private final APIClient apiClient; - private final APIClient cachedAPIClient; - - private final boolean moduleCacheEnabled; - - public APIClientFactory(APIClient apiClient, APIClient cachedAPIClient, boolean moduleCacheEnabled) { - this.apiClient = Objects.requireNonNull(apiClient); - this.cachedAPIClient = Objects.requireNonNull(cachedAPIClient); - this.moduleCacheEnabled = moduleCacheEnabled; - } - - public APIClient getClient(boolean isCacheEnabled) { - return isCacheEnabled && moduleCacheEnabled ? cachedAPIClient : apiClient; - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java index 2f33aebd181..47d879fa94d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java @@ -3,52 +3,75 @@ import io.netty.handler.codec.http.HttpResponseStatus; import io.vertx.core.Future; import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; import io.vertx.core.http.impl.headers.HeadersMultiMap; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; -import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpRequest; -import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; -import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.JacksonMapper; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.Logger; import org.prebid.server.log.LoggerFactory; import org.prebid.server.util.HttpUtil; +import org.prebid.server.validation.ValidationException; import org.prebid.server.vertx.httpclient.HttpClient; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.util.List; import java.util.Objects; -import java.util.Optional; public class APIClientImpl implements APIClient { - private static final String TENANT = "{TENANT}"; - private static final String ORIGIN = "{ORIGIN}"; private static final Logger logger = LoggerFactory.getLogger(APIClientImpl.class); private static final ConditionalLogger conditionalLogger = new ConditionalLogger(logger); + + private static final String TENANT = "{TENANT}"; + private static final String ORIGIN = "{ORIGIN}"; + private final String endpoint; private final HttpClient httpClient; + private final JacksonMapper mapper; private final double logSamplingRate; - private final OptableResponseMapper responseMapper; public APIClientImpl(String endpoint, HttpClient httpClient, - double logSamplingRate, - OptableResponseMapper responseMapper) { + JacksonMapper mapper, + double logSamplingRate) { this.endpoint = HttpUtil.validateUrl(Objects.requireNonNull(endpoint)); this.httpClient = Objects.requireNonNull(httpClient); + this.mapper = Objects.requireNonNull(mapper); this.logSamplingRate = logSamplingRate; - this.responseMapper = Objects.requireNonNull(responseMapper); } public Future getTargeting(OptableTargetingProperties properties, - Query query, List ips, long timeout) { + Query query, + List ips, + Timeout timeout) { + + final String uri = resolveEndpoint(properties.getTenant(), properties.getOrigin()); + final String queryAsString = query.toQueryString(); + final MultiMap headers = headers(properties, ips); + + try { + return httpClient.get(uri + queryAsString, headers, timeout.remaining()) + .compose(this::validateResponse) + .map(this::parseResponse) + .onFailure(exception -> logError(exception, uri)); + } catch (Exception e) { + return Future.failedFuture(e); + } + } + private String resolveEndpoint(String tenant, String origin) { + return endpoint + .replace(TENANT, tenant) + .replace(ORIGIN, origin); + } + + private static MultiMap headers(OptableTargetingProperties properties, List ips) { final MultiMap headers = HeadersMultiMap.headers() .add(HttpUtil.ACCEPT_HEADER, "application/json"); @@ -57,103 +80,34 @@ public Future getTargeting(OptableTargetingProperties propertie headers.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer %s".formatted(apiKey)); } - if (CollectionUtils.isNotEmpty(ips)) { - ips.forEach(ip -> { - headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip); - }); - } - - final HttpRequest request = HttpRequest.builder() - .uri(resolveEndpoint(endpoint, properties.getTenant(), properties.getOrigin())) - .query(query.toQueryString()) - .headers(headers) - .build(); + CollectionUtils.emptyIfNull(ips) + .forEach(ip -> headers.add(HttpUtil.X_FORWARDED_FOR_HEADER, ip)); - return doRequest(request, timeout); + return headers; } - private String resolveEndpoint(String endpointTemplate, String tenant, String origin) { - return endpointTemplate.replace(TENANT, tenant) - .replace(ORIGIN, origin); - } - - private Future doRequest(HttpRequest httpRequest, long timeout) { - return createRequest(httpRequest, timeout) - .compose(response -> processResponse(response, httpRequest)) - .recover(exception -> failResponse(exception, httpRequest)) - .compose(this::validateResponse) - .compose(it -> parseSilent(it, httpRequest)) - .recover(exception -> logParsingError(exception, httpRequest)); - } - - private Future parseSilent(OptableCall optableCall, HttpRequest httpRequest) { - try { - final TargetingResult result = responseMapper.parse(optableCall); - return result != null - ? Future.succeededFuture(result) - : Future.failedFuture("Can't parse Optable response"); - } catch (Exception e) { - logParsingError(e, httpRequest); + private Future validateResponse(HttpClientResponse response) { + if (response.getStatusCode() != HttpResponseStatus.OK.code()) { + return Future.failedFuture(new ValidationException("Invalid status code: %d", response.getStatusCode())); } - return Future.failedFuture("Can't parse Optable response"); - } - private Future logParsingError(Throwable exception, HttpRequest httpRequest) { - final String error = "Error occurred while parsing HTTP response from the Optable url: %s with message: %s" - .formatted(httpRequest.getUri(), exception.getMessage()); - logger.warn(error, logSamplingRate); - logger.debug("Error occurred while parsing HTTP response from the Optable url: {}", - exception, httpRequest.getUri()); + if (StringUtils.isBlank(response.getBody())) { + return Future.failedFuture(new ValidationException("Empty body")); + } - return Future.failedFuture(error); + return Future.succeededFuture(response); } - private Future createRequest(HttpRequest httpRequest, long remainingTimeout) { - try { - return httpClient.request(HttpMethod.GET, - httpRequest.getUri() + "&id=" + httpRequest.getQuery(), - httpRequest.getHeaders(), - (String) null, - remainingTimeout); - } catch (Exception e) { - return Future.failedFuture(e); - } + private TargetingResult parseResponse(HttpClientResponse httpResponse) { + return mapper.decodeValue(httpResponse.getBody(), TargetingResult.class); } - private static Future processResponse(HttpClientResponse response, - HttpRequest httpRequest) { + private void logError(Throwable exception, String url) { + final String errorPrefix = "Error occurred while sending HTTP request to the Optable url:"; - final int statusCode = response.getStatusCode(); - final HttpResponse httpResponse = HttpResponse.of(statusCode, response.getHeaders(), response.getBody()); - return Future.succeededFuture(OptableCall.succeededHttp(httpRequest, httpResponse)); - } - - private Future failResponse(Throwable exception, HttpRequest httpRequest) { - final String error = "Error occurred while sending HTTP request to the Optable url: %s with message: %s" - .formatted(httpRequest.getUri(), exception.getMessage()); + final String error = errorPrefix + " %s with message: %s".formatted(url, exception.getMessage()); conditionalLogger.warn(error, logSamplingRate); - logger.debug("Error occurred while sending HTTP request to the Optable url: {}", - exception, httpRequest.getUri()); - - return Future.failedFuture(error); - } - private Future validateResponse(OptableCall response) { - return Optional.ofNullable(response) - .map(OptableCall::getResponse) - .map(resp -> { - if (resp.getStatusCode() != HttpResponseStatus.OK.code()) { - final String error = "Error occurred while sending HTTP request to the " - + "Optable url: %s with message: %s".formatted(response.getRequest().getUri(), - resp.getBody()); - conditionalLogger.warn(error, logSamplingRate); - logger.debug("Error occurred while sending HTTP request to the Optable url: {}"); - - return Future.failedFuture(error); - } - - return Future.succeededFuture(response); - }) - .orElse(Future.failedFuture("OptableCall validation error")); + logger.debug(errorPrefix + " {}", exception, url); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index 8c39f07e87b..a6fa982efd4 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -1,11 +1,12 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.net; import io.vertx.core.Future; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.CacheProperties; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; @@ -19,51 +20,38 @@ public class CachedAPIClient implements APIClient { public CachedAPIClient(APIClient apiClient, Cache cache) { this.apiClient = Objects.requireNonNull(apiClient); - this.cache = cache; + this.cache = Objects.requireNonNull(cache); } - @Override public Future getTargeting(OptableTargetingProperties properties, Query query, List ips, - long timeout) { + Timeout timeout) { - final String cachingKey = createCachingKey( - properties.getTenant(), - properties.getOrigin(), - ips, - query, - true); - return cache.get(cachingKey) - .recover(ignore -> fetchAndCacheResult( - properties, - properties.getCache().getTtlseconds(), - query, - ips, - timeout)); - } + final CacheProperties cacheProperties = properties.getCache(); + if (!cacheProperties.isEnabled()) { + return apiClient.getTargeting(properties, query, ips, timeout); + } - private Future fetchAndCacheResult(OptableTargetingProperties properties, - int ttlSeconds, Query query, List ips, - long timeout) { + final String tenant = properties.getTenant(); + final String origin = properties.getOrigin(); - final String cachingKey = createCachingKey( - properties.getTenant(), - properties.getOrigin(), - ips, - query, - false); - return apiClient.getTargeting(properties, query, ips, timeout) - .compose(result -> cache.put(cachingKey, result, ttlSeconds) + return cache.get(createCachingKey(tenant, origin, ips, query, true)) + .recover(ignore -> apiClient.getTargeting(properties, query, ips, timeout) .recover(throwable -> Future.succeededFuture()) - .map(result)); + .compose(result -> cache.put( + createCachingKey(tenant, origin, ips, query, false), + result, + cacheProperties.getTtlseconds()) + .otherwiseEmpty() + .map(result))); } private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { return "%s:%s:%s:%s".formatted( tenant, origin, - OptableAttributesResolver.resolveIp(ips), + ips.getFirst(), encodeQuery ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) : query.getIds()); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java deleted file mode 100644 index 27f0a7ccc9b..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapper.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.net; - -import lombok.AllArgsConstructor; -import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.json.JacksonMapper; - -import java.util.Optional; - -@AllArgsConstructor -public class OptableResponseMapper { - - private final JacksonMapper mapper; - - public TargetingResult parse(OptableCall call) { - return Optional.ofNullable(call) - .map(OptableCall::getResponse) - .map(resp -> mapper.decodeValue(resp.getBody(), TargetingResult.class)) - .orElse(null); - } - - public TargetingResult parse(String json) { - return Optional.ofNullable(json) - .map(it -> mapper.decodeValue(it, TargetingResult.class)) - .orElse(null); - } - - public String toJsonString(TargetingResult value) { - return mapper.encodeToString(value); - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index b92ee75cb67..7074c508f53 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -33,6 +33,8 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.Privacy; @@ -55,6 +57,8 @@ public abstract class BaseOptableTest { protected final ObjectMapper mapper = ObjectMapperProvider.mapper(); + protected final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); + protected ModuleContext givenModuleContext() { return givenModuleContext(null); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 60f479771b3..1dabeb1ad03 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -49,7 +49,8 @@ public void setUp() { configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); target = new OptableTargetingAuctionResponseHook( configResolver, - mapper); + mapper, + jsonMerger); } @Test @@ -125,7 +126,8 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { List.of(new AudienceId("audienceId")), "keyspace", 1)))); target = new OptableTargetingAuctionResponseHook( configResolver, - mapper); + mapper, + jsonMerger); // when final Future> future = target.call(auctionResponsePayload, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java index 9e283f248d8..dfe98da2245 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; -import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -11,7 +10,6 @@ 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.json.ObjectMapperProvider; import java.util.Collection; import java.util.List; @@ -23,7 +21,7 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -public class OptableTargetingModuleTest { +public class OptableTargetingModuleTest extends BaseOptableTest { @Mock ConfigResolver configResolver; @@ -34,8 +32,6 @@ public class OptableTargetingModuleTest { @Mock(strictness = LENIENT) UserFpdActivityMask userFpdActivityMask; - ObjectMapper mapper = ObjectMapperProvider.mapper(); - @Test public void shouldReturnNonBlankCode() { // given @@ -60,7 +56,8 @@ public void shouldReturnHooks() { userFpdActivityMask), new OptableTargetingAuctionResponseHook( configResolver, - mapper)); + mapper, + jsonMerger)); final Module module = new OptableTargetingModule(hooks); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index a26faa7a26c..b96b4447000 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -28,7 +28,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) @@ -77,7 +76,7 @@ public void shouldHaveRightCode() { public void shouldReturnResultWithPBSAnalyticsTags() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) + when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -101,7 +100,7 @@ public void shouldReturnResultWithPBSAnalyticsTags() { public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) + when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -130,7 +129,7 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() { // given when(invocationContext.timeout()).thenReturn(timeout); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), any(), anyLong())) + when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -159,7 +158,7 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() { public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - when(optableTargeting.getTargeting(any(), any(), any(), anyLong())).thenReturn(Future.succeededFuture(null)); + when(optableTargeting.getTargeting(any(), any(), any(), any())).thenReturn(Future.succeededFuture(null)); // when final Future> future = target.call(auctionRequestPayload, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java index ad274002544..74cc3668dff 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java @@ -20,8 +20,8 @@ public void shouldEnrichBidResponseByTargetingKeywords() { final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(givenBidResponse()); // when - final AuctionResponsePayload result = BidResponseEnricher.of( - mapper, givenTargeting()).apply(auctionResponsePayload); + final AuctionResponsePayload result = BidResponseEnricher.of(givenTargeting(), mapper, jsonMerger) + .apply(auctionResponsePayload); final ObjectNode targeting = (ObjectNode) result.bidResponse().getSeatbid() .getFirst() .getBid() @@ -41,7 +41,8 @@ public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(givenBidResponse()); // when - final AuctionResponsePayload result = BidResponseEnricher.of(mapper, null).apply(auctionResponsePayload); + final AuctionResponsePayload result = BidResponseEnricher.of(null, mapper, jsonMerger) + .apply(auctionResponsePayload); final ObjectNode targeting = (ObjectNode) result.bidResponse().getSeatbid() .getFirst() .getBid() @@ -55,19 +56,6 @@ public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { assertThat(targeting.get("keyspace")).isNull(); } - @Test - public void shouldNotFailWhenResponseIsNull() { - // given - final AuctionResponsePayload auctionResponsePayload = AuctionResponsePayloadImpl.of(null); - - // when - final AuctionResponsePayload result = BidResponseEnricher.of(mapper, givenTargeting()) - .apply(auctionResponsePayload); - - // then - assertThat(result.bidResponse()).isNull(); - } - private List givenTargeting() { return List.of( new Audience("provider", diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java index 7dbfda47a71..ccb6ac3fee5 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.cache.PbcStorageService; import org.prebid.server.cache.proto.request.module.StorageDataType; @@ -15,7 +16,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableResponseMapper; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.ObjectMapperProvider; @@ -24,7 +24,6 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -34,15 +33,16 @@ public class CacheTest { @Mock private PbcStorageService pbcStorageService; + @Spy + private JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper()); private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); - private final OptableResponseMapper optableResponseMapper = spy(new OptableResponseMapper(mapper)); private Cache target; @BeforeEach public void setUp() { - target = new Cache(pbcStorageService, optableResponseMapper); + target = new Cache(pbcStorageService, jacksonMapper); } @Test @@ -56,7 +56,7 @@ public void cacheShouldNotCallMapperIfNoEntry() { // then Assertions.assertThat(result).isNull(); - verify(optableResponseMapper, times(0)).parse(anyString()); + verify(jacksonMapper, times(0)).decodeValue(anyString(), eq(TargetingResult.class)); } @Test @@ -75,7 +75,7 @@ public void cacheShouldReturnEntry() { .isNotNull() .isEqualTo(targetingResult); - verify(optableResponseMapper, times(1)).parse(anyString()); + verify(jacksonMapper, times(1)).decodeValue(anyString(), eq(TargetingResult.class)); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index d82f932cf43..8a8029c09f2 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.iab.gpp.encoder.GppModel; +import com.iab.openrtb.request.BidRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -53,7 +54,7 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() { when(tcfContext.getConsentString()).thenReturn("consent"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, @@ -71,9 +72,10 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { final GppModel gppModel = mock(); when(tcfContext.isConsentValid()).thenReturn(false); when(tcfContext.getConsentString()).thenReturn("consent"); + when(tcfContext.getIpAddress()).thenReturn("8.8.8.8"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, @@ -94,7 +96,7 @@ public void shouldResolveGppAttributes() { when(tcfContext.getConsentString()).thenReturn("consent"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(tcfContext, gppContext); + final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, @@ -117,8 +119,9 @@ public AuctionContext givenAuctionContext(GppContext gppContext) { return AuctionContext.builder().gppContext(gppContext).build(); } - public AuctionContext givenAuctionContext(TcfContext tcfContext, GppContext gppContext) { + public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { return AuctionContext.builder() + .bidRequest(bidRequest) .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) .gppContext(gppContext).build(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 6e159275382..9413608071e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -7,12 +7,12 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientFactory; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; @@ -21,7 +21,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,11 +37,9 @@ public class OptableTargetingTest extends BaseOptableTest { private APIClientImpl apiClient; @Mock - private CachedAPIClient cachingAPIClient; - - private APIClientFactory apiClientFactory; + private Timeout timeout; - private QueryBuilder queryBuilder = new QueryBuilder(); + private CachedAPIClient cachingAPIClient; private OptableTargeting target; @@ -52,28 +49,28 @@ public class OptableTargetingTest extends BaseOptableTest { @BeforeEach public void setUp() { - apiClientFactory = new APIClientFactory(apiClient, cachingAPIClient, true); optableAttributes = givenOptableAttributes(); properties = givenOptableTargetingProperties(true); - target = new OptableTargeting(idsMapper, queryBuilder, apiClientFactory); + cachingAPIClient = new CachedAPIClient(apiClient, cache); + target = new OptableTargeting(idsMapper, cachingAPIClient); } @Test - public void shouldUseNonCachedAPIClient() { + public void shouldCallNonCachedAPIClient() { // given final BidRequest bidRequest = givenBidRequest(); properties = givenOptableTargetingProperties(false); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when final Future targetingResult = target.getTargeting( - properties, bidRequest, optableAttributes, 100); + properties, bidRequest, optableAttributes, timeout); // then assertThat(targetingResult.result()).isNotNull(); - verify(apiClient).getTargeting(any(), any(), any(), anyLong()); + verify(apiClient).getTargeting(any(), any(), any(), any()); } @Test @@ -82,21 +79,23 @@ public void shouldUseCachedAPIClient() { final BidRequest bidRequest = givenBidRequest(); properties = givenOptableTargetingProperties(true); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(cachingAPIClient.getTargeting(any(), any(), any(), anyLong())) + when(cache.get(any())).thenReturn(Future.failedFuture(new NullPointerException())); + when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when final Future targetingResult = target.getTargeting( - properties, bidRequest, optableAttributes, 100); + properties, bidRequest, optableAttributes, timeout); // then - assertThat(targetingResult.result()).isNotNull(); - verify(cachingAPIClient).getTargeting(any(), any(), any(), anyLong()); + verify(cache).get(any()); + verify(apiClient).getTargeting(any(), any(), any(), any()); } private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .gpp("gpp") + .ips(List.of("8.8.8.8")) .gppSid(Set.of(2)) .build(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 057c4c1fde3..b4ac5a740e4 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -12,8 +12,6 @@ public class QueryBuilderTest { - private QueryBuilder target; - private OptableAttributes optableAttributes; private String idPrefixOrder; @@ -21,7 +19,6 @@ public class QueryBuilderTest { @BeforeEach public void setUp() { optableAttributes = givenOptableAttributes(); - target = new QueryBuilder(); idPrefixOrder = "c,c1"; } @@ -31,10 +28,10 @@ public void shouldSeparateAttributesFromIds() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final Query query = target.build(ids, optableAttributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); // then - assertThat(query.getIds()).isEqualTo("e%3Aemail&id=p%3A123"); + assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms"); } @@ -44,10 +41,10 @@ public void shouldBuildFullQueryString() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final Query query = target.build(ids, optableAttributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); // then - assertThat(query.getIds()).isEqualTo("e%3Aemail&id=p%3A123"); + assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms"); } @@ -57,7 +54,7 @@ public void shouldBuildQueryStringWhenHaveIds() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then assertThat(query).contains("e%3Aemail", "p%3A123"); @@ -69,7 +66,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); @@ -82,10 +79,10 @@ public void shouldBuildQueryStringWithRightOrder() { Id.of("c", "234")); // when - final String query = target.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); // then - assertThat(query).startsWith("c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); + assertThat(query).startsWith("&id=c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); } @Test @@ -97,7 +94,7 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { .build(); // when - final Query query = target.build(ids, attributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); // then @@ -113,7 +110,7 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { .build(); // when - final Query query = target.build(ids, attributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); // then diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java deleted file mode 100644 index e95b39d47f9..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/merger/ExtMergerTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.core.merger; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.node.ObjectNode; -import org.junit.jupiter.api.Test; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; - -public class ExtMergerTest { - - protected final ObjectMapper mapper = new ObjectMapper(); - - @Test - public void shouldMergeTwoExtObjects() { - // given - final ObjectNode destination = givenExt(Map.of("field1", "value1", "field2", "value2")); - final ObjectNode source = givenExt(Map.of("field3", "value3", "field4", "value4")); - - // when - final ObjectNode result = ExtMerger.mergeExt(destination, source); - - // then - assertThat(result).isNotNull().hasSize(4); - assertThat(result.get("field1").asText()).isEqualTo("value1"); - assertThat(result.get("field2").asText()).isEqualTo("value2"); - assertThat(result.get("field3").asText()).isEqualTo("value3"); - assertThat(result.get("field4").asText()).isEqualTo("value4"); - } - - @Test - public void shouldUseFirstArgumentWhenSecondIsNull() { - // given - final ObjectNode destination = givenExt(Map.of("field1", "value1", "field2", "value2")); - - // when - final ObjectNode result = ExtMerger.mergeExt(destination, null); - - // then - assertThat(result).isNotNull().hasSize(2); - assertThat(result.get("field1").asText()).isEqualTo("value1"); - assertThat(result.get("field2").asText()).isEqualTo("value2"); - } - - @Test - public void shouldUseSecondArgumentWhenFirstIsNull() { - // given - final ObjectNode source = givenExt(Map.of("field1", "value1", "field2", "value2")); - - // when - final ObjectNode result = ExtMerger.mergeExt(null, source); - - // then - assertThat(result).isNotNull().hasSize(2); - assertThat(result.get("field1").asText()).isEqualTo("value1"); - assertThat(result.get("field2").asText()).isEqualTo("value2"); - } - - @Test - public void shouldNotFail() { - // given and when - final ObjectNode result = ExtMerger.mergeExt(null, null); - - // then - assertThat(result).isNull(); - } - - protected ObjectNode givenExt(Map fields) { - final ObjectNode ext = mapper.createObjectNode(); - fields.forEach(ext::put); - - return ext; - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java index 2ebae99a349..3cf07190974 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java @@ -2,7 +2,6 @@ import io.vertx.core.Future; import io.vertx.core.MultiMap; -import io.vertx.core.http.HttpMethod; import org.apache.http.HttpStatus; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,11 +9,11 @@ import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.vertx.httpclient.HttpClient; import java.util.List; @@ -22,8 +21,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -41,25 +38,27 @@ public class APIClientTest extends BaseOptableTest { @Mock private HttpClient httpClient; + @Mock + private Timeout timeout; + private APIClient target; - private final OptableResponseMapper parser = new OptableResponseMapper( - new JacksonMapper(ObjectMapperProvider.mapper())); + private final JacksonMapper jacksonMapper = new JacksonMapper(mapper); @BeforeEach public void setUp() { - target = new APIClientImpl("http://endpoint.optable.com", httpClient, LOG_SAMPLING_RATE, parser); + target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, LOG_SAMPLING_RATE); } @Test public void shouldReturnTargetingResult() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), timeout); // then assertThat(result.result()).isNotNull(); @@ -72,12 +71,12 @@ public void shouldReturnTargetingResult() { @Test public void shouldReturnNullWhenEndpointRespondsWithError() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), timeout); // then assertThat(result.result()).isNull(); @@ -86,12 +85,12 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { @Test public void shouldNotFailWhenEndpointRespondsWithWrongData() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), timeout); // then assertThat(result.result()).isNull(); @@ -100,12 +99,11 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { @Test public void shouldNotFailWhenHttpClientIsCrashed() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) - .thenThrow(new NullPointerException()); + when(httpClient.get(any(), any(), anyLong())).thenThrow(new NullPointerException()); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), timeout); // then assertThat(result.result()).isNull(); @@ -114,13 +112,13 @@ public void shouldNotFailWhenHttpClientIsCrashed() { @Test public void shouldNotFailWhenInternalErrorOccurs() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), timeout); // then assertThat(result.result()).isNull(); @@ -129,23 +127,19 @@ public void shouldNotFailWhenInternalErrorOccurs() { @Test public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // given - target = new APIClientImpl("http://endpoint.optable.com", - httpClient, - LOG_SAMPLING_RATE, - parser); + target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, LOG_SAMPLING_RATE); - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), 1000); + givenQuery(), List.of("8.8.8.8"), timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); - verify(httpClient) - .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); assertThat(headersCaptor.getValue().get(ACCEPT_HEADER)).isEqualTo("application/json"); assertThat(headersCaptor.getValue().get(AUTHORIZATION_HEADER)).isEqualTo("Bearer key"); assertThat(result.result()).isNull(); @@ -154,7 +148,7 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { @Test public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); @@ -163,12 +157,11 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { givenOptableTargetingProperties(null, false), givenQuery(), List.of("8.8.8.8"), - 1000); + timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); - verify(httpClient) - .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); assertThat(headersCaptor.getValue().get(ACCEPT_HEADER)).isEqualTo("application/json"); assertThat(headersCaptor.getValue().get(AUTHORIZATION_HEADER)).isNull(); assertThat(result.result()).isNull(); @@ -177,7 +170,7 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { @Test public void shouldPassThroughIpAddresses() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); @@ -186,12 +179,11 @@ public void shouldPassThroughIpAddresses() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8", "2001:4860:4860::8888"), - 1000); + timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); - verify(httpClient) - .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); assertThat(headersCaptor.getValue().getAll(X_FORWARDED_FOR_HEADER)).contains("8.8.8.8", "2001:4860:4860::8888"); assertThat(result.result()).isNull(); } @@ -199,18 +191,17 @@ public void shouldPassThroughIpAddresses() { @Test public void shouldNotPassThroughIpAddressWhenNotSpecified() { // given - when(httpClient.request(eq(HttpMethod.GET), any(), any(), nullable(String.class), anyLong())) + when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), null, 1000); + givenQuery(), null, timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); - verify(httpClient) - .request(eq(HttpMethod.GET), any(), headersCaptor.capture(), nullable(String.class), anyLong()); + verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); assertThat(headersCaptor.getValue().get(X_FORWARDED_FOR_HEADER)).isNull(); assertThat(result.result()).isNull(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java index 5e2a8cf07e7..45223795aea 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Query; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; @@ -18,7 +19,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -33,6 +33,9 @@ public class CachedAPIClientTest extends BaseOptableTest { @Mock private APIClientImpl apiClient; + @Mock(strictness = Mock.Strictness.LENIENT) + private Timeout timeout; + private APIClient target; private OptableTargetingProperties properties; @@ -41,6 +44,7 @@ public class CachedAPIClientTest extends BaseOptableTest { public void setUp() { properties = givenOptableTargetingProperties(true); target = new CachedAPIClient(apiClient, cache); + when(timeout.remaining()).thenReturn(1000L); } @Test @@ -49,12 +53,12 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { when(cache.get(any())).thenReturn(Future.failedFuture("error")); when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final Query query = givenQuery(); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), 100); + properties, query, List.of("8.8.8.8"), timeout); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -72,12 +76,12 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final Query query = givenQuery(); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), 100); + properties, query, List.of("8.8.8.8"), timeout); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -86,7 +90,7 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(apiClient, times(1)).getTargeting(any(), any(), any(), anyLong()); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), any()); verify(cache).put(any(), eq(targetingResult.result()), anyInt()); } @@ -98,7 +102,7 @@ public void shouldUseCachedResult() { // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), 100); + properties, query, List.of("8.8.8.8"), timeout); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -108,7 +112,7 @@ public void shouldUseCachedResult() { .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); verify(cache, times(1)).get(any()); - verify(apiClient, times(0)).getTargeting(any(), any(), any(), anyLong()); + verify(apiClient, times(0)).getTargeting(any(), any(), any(), any()); verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @@ -118,11 +122,12 @@ public void shouldNotFailWhenApiClientIsFailed() { properties = givenOptableTargetingProperties(false); final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); - when(apiClient.getTargeting(any(), any(), any(), anyLong())).thenReturn(null); + when(apiClient.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.failedFuture(new NullPointerException())); // when final Future targetingResult = target.getTargeting(properties, query, - List.of("8.8.8.8"), 100); + List.of("8.8.8.8"), timeout); // then assertThat(targetingResult.result()).isNull(); @@ -134,12 +139,12 @@ public void shouldNotFailWhenApiClientReturnsFailFuture() { properties = givenOptableTargetingProperties(false); final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); - when(apiClient.getTargeting(any(), any(), any(), anyLong())) + when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.failedFuture("File")); // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), 100); + properties, query, List.of("8.8.8.8"), timeout); // then assertThat(targetingResult.result()).isNull(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java deleted file mode 100644 index dae577dbf8e..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableResponseMapperTest.java +++ /dev/null @@ -1,96 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.net; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.prebid.server.hooks.modules.optable.targeting.model.net.HttpResponse; -import org.prebid.server.hooks.modules.optable.targeting.model.net.OptableCall; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; -import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; -import org.prebid.server.json.DecodeException; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.ObjectMapperProvider; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertThrows; - -public class OptableResponseMapperTest extends BaseOptableTest { - - private OptableResponseMapper target; - - private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); - - @BeforeEach - public void setUp() { - - target = new OptableResponseMapper(mapper); - } - - @Test - public void shouldNotFailWhenSourceIsNull() { - // given and when - final TargetingResult result = target.parse((OptableCall) null); - - //then - assertThat(result).isNull(); - } - - @Test - public void shouldNotFailWhenResponseIsNull() { - // given - final OptableCall optableCall = OptableCall.succeededHttp(null, null); - - //when - final TargetingResult result = target.parse(optableCall); - - //then - assertThat(result).isNull(); - } - - @Test - public void shouldNotFailWhenResponseBodyIsWrong() { - // given - final HttpResponse response = givenSuccessResponse("{\"field'\": \"value\"}"); - final OptableCall optableCall = OptableCall.succeededHttp(null, response); - - //when - final TargetingResult result = target.parse(optableCall); - - //then - assertThat(result).isNotNull(); - assertThat(result.getOrtb2()).isNull(); - } - - @Test - public void shouldParseRightResponse() { - // given - final HttpResponse response = givenSuccessResponse(givenBodyFromFile("targeting_response.json")); - final OptableCall optableCall = OptableCall.succeededHttp(null, response); - - //when - final TargetingResult result = target.parse(optableCall); - - //then - assertThat(result).isNotNull(); - final User user = result.getOrtb2().getUser(); - assertThat(user.getEids().getFirst().getUids().getFirst().getId()).isEqualTo("uid_id1"); - assertThat(user.getData().getFirst().getSegment().getFirst().getId()).isEqualTo("segment_id"); - } - - @Test - public void shouldFailWhenGotNotJsonString() { - // given - final HttpResponse response = givenSuccessResponse("random string"); - final OptableCall optableCall = OptableCall.succeededHttp(null, response); - - //when and then - assertThrows( - DecodeException.class, - () -> target.parse(optableCall) - ); - } - - private HttpResponse givenSuccessResponse(String body) { - return HttpResponse.of(200, null, body); - } -} From 01423ec7faefe63bd4baa1975e29c96632d67cb6 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 7 Jul 2025 09:06:39 +0200 Subject: [PATCH 36/41] optable-targeting: Remove custom Http client --- .../config/OptableTargetingConfig.java | 35 ++---- .../targeting/model/net/HttpRequest.java | 16 --- .../targeting/model/net/HttpResponse.java | 14 --- .../targeting/model/net/OptableCall.java | 11 -- .../targeting/v1/net/CachedAPIClient.java | 11 +- .../v1/net/OptableHttpClientWrapper.java | 48 -------- .../v1/core/OptableTargetingTest.java | 2 +- .../targeting/v1/net/CachedAPIClientTest.java | 23 ++-- .../v1/net/OptableHttpClientWrapperTest.java | 114 ------------------ 9 files changed, 37 insertions(+), 237 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 7a3faca8427..8f35e622062 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.config; -import io.vertx.core.Vertx; import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.cache.PbcStorageService; @@ -14,20 +13,16 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; -import org.prebid.server.hooks.modules.optable.targeting.v1.net.OptableHttpClientWrapper; import org.prebid.server.json.JacksonMapper; import org.prebid.server.json.JsonMerger; import org.prebid.server.json.ObjectMapperProvider; -import org.prebid.server.spring.config.VertxContextScope; -import org.prebid.server.spring.config.model.HttpClientProperties; +import org.prebid.server.vertx.httpclient.HttpClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Scope; -import org.springframework.context.annotation.ScopedProxyMode; import java.util.List; @@ -47,31 +42,27 @@ IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") doubl } @Bean - @Scope(scopeName = VertxContextScope.NAME, proxyMode = ScopedProxyMode.TARGET_CLASS) - @ConditionalOnProperty(prefix = "http-client.circuit-breaker", name = "enabled", havingValue = "false", - matchIfMissing = true) - OptableHttpClientWrapper optableHttpClient(Vertx vertx, HttpClientProperties httpClientProperties) { - return new OptableHttpClientWrapper(vertx, httpClientProperties); - } - - @Bean - APIClientImpl apiClient(OptableHttpClientWrapper httpClientWrapper, - @Value("${logging.sampling-rate:0.01}") - double logSamplingRate, - OptableTargetingProperties properties, - JacksonMapper jacksonMapperr) { + APIClientImpl apiClient(HttpClient httpClient, + @Value("${logging.sampling-rate:0.01}") + double logSamplingRate, + OptableTargetingProperties properties, + JacksonMapper jacksonMapperr) { return new APIClientImpl( properties.getApiEndpoint(), - httpClientWrapper.getHttpClient(), + httpClient, jacksonMapperr, logSamplingRate); } @Bean @ConditionalOnProperty(name = {"storage.pbc.enabled", "cache.module.enabled"}, havingValue = "true") - CachedAPIClient cachedApiClient(APIClientImpl apiClient, Cache cache) { - return new CachedAPIClient(apiClient, cache); + CachedAPIClient cachedApiClient(APIClientImpl apiClient, + Cache cache, + @Value("${http-client.circuit-breaker.enabled:false}") + Boolean isCircuitBreakerEnabled) { + + return new CachedAPIClient(apiClient, cache, isCircuitBreakerEnabled); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java deleted file mode 100644 index 90782e05041..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpRequest.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model.net; - -import io.vertx.core.MultiMap; -import lombok.Builder; -import lombok.Value; - -@Builder -@Value -public class HttpRequest { - - String uri; - - String query; - - MultiMap headers; -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java deleted file mode 100644 index 172b09265ec..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/HttpResponse.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model.net; - -import io.vertx.core.MultiMap; -import lombok.Value; - -@Value(staticConstructor = "of") -public class HttpResponse { - - int statusCode; - - MultiMap headers; - - String body; -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java deleted file mode 100644 index 45f242bc983..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/net/OptableCall.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.model.net; - -import lombok.Value; - -@Value(staticConstructor = "succeededHttp") -public class OptableCall { - - HttpRequest request; - - HttpResponse response; -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index a6fa982efd4..b00d71315cf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -17,10 +17,12 @@ public class CachedAPIClient implements APIClient { private final APIClient apiClient; private final Cache cache; + private final boolean isCircuitBreakerEnabled; - public CachedAPIClient(APIClient apiClient, Cache cache) { + public CachedAPIClient(APIClient apiClient, Cache cache, boolean isCircuitBreakerEnabled) { this.apiClient = Objects.requireNonNull(apiClient); this.cache = Objects.requireNonNull(cache); + this.isCircuitBreakerEnabled = isCircuitBreakerEnabled; } public Future getTargeting(OptableTargetingProperties properties, @@ -37,8 +39,11 @@ public Future getTargeting(OptableTargetingProperties propertie final String origin = properties.getOrigin(); return cache.get(createCachingKey(tenant, origin, ips, query, true)) - .recover(ignore -> apiClient.getTargeting(properties, query, ips, timeout) - .recover(throwable -> Future.succeededFuture()) + .recover(ignore -> + apiClient.getTargeting(properties, query, ips, timeout) + .recover(throwable -> isCircuitBreakerEnabled + ? Future.succeededFuture(new TargetingResult(null, null)) + : Future.failedFuture(throwable)) .compose(result -> cache.put( createCachingKey(tenant, origin, ips, query, false), result, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java deleted file mode 100644 index 8da7722c2aa..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapper.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.net; - -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpClientOptions; -import io.vertx.core.net.JksOptions; -import org.prebid.server.spring.config.model.HttpClientProperties; -import org.prebid.server.vertx.httpclient.BasicHttpClient; -import org.prebid.server.vertx.httpclient.HttpClient; - -import java.util.concurrent.TimeUnit; - -public class OptableHttpClientWrapper { - - private HttpClient httpClient; - - public OptableHttpClientWrapper(Vertx vertx, HttpClientProperties httpClientProperties) { - this.httpClient = createBasicHttpClient(vertx, httpClientProperties); - } - - public HttpClient getHttpClient() { - return httpClient; - } - - private static HttpClient createBasicHttpClient(Vertx vertx, HttpClientProperties httpClientProperties) { - final HttpClientOptions options = new HttpClientOptions() - .setMaxPoolSize(httpClientProperties.getMaxPoolSize()) - .setIdleTimeoutUnit(TimeUnit.MILLISECONDS) - .setIdleTimeout(httpClientProperties.getIdleTimeoutMs()) - .setPoolCleanerPeriod(httpClientProperties.getPoolCleanerPeriodMs()) - .setTryUseCompression(httpClientProperties.getUseCompression()) - .setConnectTimeout(httpClientProperties.getConnectTimeoutMs()) - // Vert.x's HttpClientRequest needs this value to be 2 for redirections to be followed once, - // 3 for twice, and so on - .setMaxRedirects(httpClientProperties.getMaxRedirects() + 1); - - if (httpClientProperties.getSsl()) { - final JksOptions jksOptions = new JksOptions() - .setPath(httpClientProperties.getJksPath()) - .setPassword(httpClientProperties.getJksPassword()); - - options - .setSsl(true) - .setKeyStoreOptions(jksOptions); - } - - return new BasicHttpClient(vertx, vertx.createHttpClient(options)); - } -} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 9413608071e..e201891200f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -51,7 +51,7 @@ public class OptableTargetingTest extends BaseOptableTest { public void setUp() { optableAttributes = givenOptableAttributes(); properties = givenOptableTargetingProperties(true); - cachingAPIClient = new CachedAPIClient(apiClient, cache); + cachingAPIClient = new CachedAPIClient(apiClient, cache, false); target = new OptableTargeting(idsMapper, cachingAPIClient); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java index 45223795aea..5b04f881579 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -43,7 +43,7 @@ public class CachedAPIClientTest extends BaseOptableTest { @BeforeEach public void setUp() { properties = givenOptableTargetingProperties(true); - target = new CachedAPIClient(apiClient, cache); + target = new CachedAPIClient(apiClient, cache, false); when(timeout.remaining()).thenReturn(1000L); } @@ -119,7 +119,7 @@ public void shouldUseCachedResult() { @Test public void shouldNotFailWhenApiClientIsFailed() { // given - properties = givenOptableTargetingProperties(false); + properties = givenOptableTargetingProperties(true); final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); when(apiClient.getTargeting(any(), any(), any(), any())) @@ -131,22 +131,29 @@ public void shouldNotFailWhenApiClientIsFailed() { // then assertThat(targetingResult.result()).isNull(); + verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @Test - public void shouldNotFailWhenApiClientReturnsFailFuture() { + public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { // given - properties = givenOptableTargetingProperties(false); + properties = givenOptableTargetingProperties(true); final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); when(apiClient.getTargeting(any(), any(), any(), any())) - .thenReturn(Future.failedFuture("File")); + .thenReturn(Future.failedFuture(new NullPointerException())); + when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); // when - final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), timeout); + target = new CachedAPIClient(apiClient, cache, true); + final Future targetingResult = target.getTargeting(properties, query, + List.of("8.8.8.8"), timeout); // then - assertThat(targetingResult.result()).isNull(); + final TargetingResult result = targetingResult.result(); + assertThat(result).isNotNull(); + assertThat(result.getOrtb2()).isNull(); + assertThat(result.getAudience()).isNull(); + verify(cache, times(1)).put(any(), eq(targetingResult.result()), anyInt()); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java deleted file mode 100644 index a188697be27..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/OptableHttpClientWrapperTest.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1.net; - -import io.vertx.core.Future; -import io.vertx.core.MultiMap; -import io.vertx.core.Vertx; -import io.vertx.core.buffer.Buffer; -import io.vertx.core.http.HttpClientOptions; -import io.vertx.core.http.HttpClientRequest; -import io.vertx.core.http.HttpClientResponse; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.RequestOptions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.spring.config.model.HttpClientProperties; -import org.prebid.server.vertx.httpclient.HttpClient; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class OptableHttpClientWrapperTest { - - @Mock - private Vertx vertx; - - @Mock(strictness = LENIENT) - io.vertx.core.http.HttpClient httpClient; - - @Mock(strictness = LENIENT) - private HttpClientRequest httpClientRequest; - - @Mock - private HttpClientResponse httpClientResponse; - - private HttpClientProperties httpClientProperties; - - private OptableHttpClientWrapper target; - - @BeforeEach - public void setUp() { - when(httpClient.request(any())).thenReturn(Future.succeededFuture(httpClientRequest)); - given(httpClientRequest.send()).willReturn(Future.succeededFuture(httpClientResponse)); - given(httpClientRequest.send(any(Buffer.class))).willReturn(Future.succeededFuture(httpClientResponse)); - when(vertx.createHttpClient(any(HttpClientOptions.class))).thenReturn(httpClient); - httpClientProperties = giveHttpClientProperties(); - target = new OptableHttpClientWrapper(vertx, httpClientProperties); - } - - @Test - public void shouldCreateHttpClient() { - // given nad when - final HttpClient httpClient = target.getHttpClient(); - - // then - assertThat(httpClient).isNotNull(); - final ArgumentCaptor httpClientOptionsCaptor = - ArgumentCaptor.forClass(HttpClientOptions.class); - verify(vertx).createHttpClient(httpClientOptionsCaptor.capture()); - - assertThat(httpClientOptionsCaptor.getValue()).satisfies(options -> { - assertThat(options.getMaxPoolSize()).isEqualTo(8); - assertThat(options.isKeepAlive()).isEqualTo(true); - assertThat(options.getKeepAliveTimeout()).isEqualTo(60); - assertThat(options.getHttp2KeepAliveTimeout()).isEqualTo(60); - assertThat(options.isTryUseCompression()).isEqualTo(true); - assertThat(options.isSsl()).isEqualTo(true); - }); - } - - @Test - public void requestShouldPerformHttpRequestWithExpectedParams() { - // given and when - target.getHttpClient().request(HttpMethod.POST, "http://www.example.com", - MultiMap.caseInsensitiveMultiMap(), "body", 500L); - - // then - final ArgumentCaptor requestOptionsArgumentCaptor = - ArgumentCaptor.forClass(RequestOptions.class); - verify(httpClient).request(requestOptionsArgumentCaptor.capture()); - - final RequestOptions expectedRequestOptions = new RequestOptions() - .setFollowRedirects(true) - .setConnectTimeout(500L) - .setMethod(HttpMethod.POST) - .setAbsoluteURI("http://www.example.com") - .setHeaders(MultiMap.caseInsensitiveMultiMap()); - assertThat(requestOptionsArgumentCaptor.getValue().toJson()).isEqualTo(expectedRequestOptions.toJson()); - - verify(httpClientRequest).send(eq(Buffer.buffer("body".getBytes()))); - } - - private HttpClientProperties giveHttpClientProperties() { - final HttpClientProperties properties = new HttpClientProperties(); - properties.setMaxPoolSize(8); - properties.setConnectTimeoutMs(2000); - properties.setPoolCleanerPeriodMs(6000); - properties.setIdleTimeoutMs(200); - properties.setUseCompression(true); - properties.setSsl(true); - properties.setJksPath("/some_path"); - properties.setJksPassword("password"); - properties.setMaxRedirects(2); - return properties; - } -} From 320a9526d7279ba528e91b68f0ad293ccb402a55 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 7 Jul 2025 16:42:28 +0200 Subject: [PATCH 37/41] optable-targeting: Code cleanup --- .../modules/optable/targeting/v1/core/BidRequestEnricher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index d7fa0335b4e..b3ea3e6dce9 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -60,14 +60,14 @@ private BidRequest enrichBidRequest(BidRequest bidRequest) { .build(); } - private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { + private static com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { return user.toBuilder() .eids(mergeEids(user.getEids(), optableUser.getEids())) .data(mergeData(user.getData(), optableUser.getData())) .build(); } - private List mergeEids(List destination, List source) { + private static List mergeEids(List destination, List source) { return merge( destination, source, From 43974a5523b3f4de8da9dc7510fa506c223fc663 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 14 Jul 2025 14:23:27 +0200 Subject: [PATCH 38/41] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 2 +- .../OptableTargetingAuctionResponseHook.java | 1 - .../v1/core/OptableAttributesResolver.java | 2 +- .../targeting/v1/net/APIClientImpl.java | 12 +-- .../targeting/v1/net/CachedAPIClient.java | 5 +- .../optable/targeting/v1/BaseOptableTest.java | 25 ++--- ...tableTargetingAuctionResponseHookTest.java | 49 +++++---- ...getingProcessedAuctionRequestHookTest.java | 38 +++---- .../v1/core/AuctionResponseValidatorTest.java | 5 +- .../v1/core/BidRequestEnricherTest.java | 39 ++++---- .../v1/core/BidResponseEnricherTest.java | 14 ++- .../optable/targeting/v1/core/CacheTest.java | 13 ++- .../{IdMapperTest.java => IdsMapperTest.java} | 83 ++++++++-------- .../core/OptableAttributesResolverTest.java | 33 ++----- .../v1/core/OptableTargetingTest.java | 35 +++---- .../targeting/v1/core/QueryBuilderTest.java | 19 ++-- ...ClientTest.java => APIClientImplTest.java} | 99 ++++++++++--------- .../targeting/v1/net/CachedAPIClientTest.java | 43 ++++---- 18 files changed, 247 insertions(+), 270 deletions(-) rename extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/{IdMapperTest.java => IdsMapperTest.java} (85%) rename extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/{APIClientTest.java => APIClientImplTest.java} (69%) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 8f35e622062..608567af2b4 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -60,7 +60,7 @@ APIClientImpl apiClient(HttpClient httpClient, CachedAPIClient cachedApiClient(APIClientImpl apiClient, Cache cache, @Value("${http-client.circuit-breaker.enabled:false}") - Boolean isCircuitBreakerEnabled) { + boolean isCircuitBreakerEnabled) { return new CachedAPIClient(apiClient, cache, isCircuitBreakerEnabled); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 2139c5ccacc..5a20f79a347 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -31,7 +31,6 @@ public class OptableTargetingAuctionResponseHook implements AuctionResponseHook private final ConfigResolver configResolver; private final ObjectMapper objectMapper; - private final JsonMerger jsonMerger; public OptableTargetingAuctionResponseHook(ConfigResolver configResolver, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 4b635b6c84e..6fda659278f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -47,7 +47,7 @@ public static List resolveIp(AuctionContext auctionContext) { deviceOpt.map(Device::getIpv6).ifPresent(result::add); if (result.isEmpty()) { - Optional.ofNullable(auctionContext.getPrivacyContext().getTcfContext().getIpAddress()) + Optional.ofNullable(auctionContext.getPrivacyContext().getIpAddress()) .ifPresent(result::add); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java index 47d879fa94d..e0e6ac94fec 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java @@ -55,14 +55,10 @@ public Future getTargeting(OptableTargetingProperties propertie final String queryAsString = query.toQueryString(); final MultiMap headers = headers(properties, ips); - try { - return httpClient.get(uri + queryAsString, headers, timeout.remaining()) - .compose(this::validateResponse) - .map(this::parseResponse) - .onFailure(exception -> logError(exception, uri)); - } catch (Exception e) { - return Future.failedFuture(e); - } + return httpClient.get(uri + queryAsString, headers, timeout.remaining()) + .compose(this::validateResponse) + .map(this::parseResponse) + .onFailure(exception -> logError(exception, uri)); } private String resolveEndpoint(String tenant, String origin) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index b00d71315cf..73ade87e5e8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -39,13 +39,12 @@ public Future getTargeting(OptableTargetingProperties propertie final String origin = properties.getOrigin(); return cache.get(createCachingKey(tenant, origin, ips, query, true)) - .recover(ignore -> - apiClient.getTargeting(properties, query, ips, timeout) + .recover(ignore -> apiClient.getTargeting(properties, query, ips, timeout) .recover(throwable -> isCircuitBreakerEnabled ? Future.succeededFuture(new TargetingResult(null, null)) : Future.failedFuture(throwable)) .compose(result -> cache.put( - createCachingKey(tenant, origin, ips, query, false), + createCachingKey(tenant, origin, ips, query, false), result, cacheProperties.getTtlseconds()) .otherwiseEmpty() diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 7074c508f53..8a5c653cbcb 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -88,7 +88,7 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr } protected BidRequest givenBidRequest() { - return givenBidRequestWithUserEids((List) null); + return givenBidRequestWithUserEids(null); } protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { @@ -122,9 +122,7 @@ protected BidResponse givenBidResponse() { return BidResponse.builder() .seatbid(List.of(SeatBid.builder() - .bid(List.of(Bid.builder() - .ext(bidExtNode) - .build())) + .bid(List.of(Bid.builder().ext(bidExtNode).build())) .build())) .build(); } @@ -138,17 +136,14 @@ protected TargetingResult givenTargetingResultWithData(List data) { } protected TargetingResult givenTargetingResult() { - return givenTargetingResult(List.of(Eid.builder() + return givenTargetingResult( + List.of(Eid.builder() .source("source") - .uids(List.of(Uid.builder() - .id("id") - .build())) + .uids(List.of(Uid.builder().id("id").build())) .build()), List.of(Data.builder() .id("id") - .segment(List.of(Segment.builder() - .id("id") - .build())) + .segment(List.of(Segment.builder().id("id").build())) .build())); } @@ -158,12 +153,8 @@ protected TargetingResult givenTargetingResult(List eids, List data) "provider", List.of(new AudienceId("id")), "keyspace", - 1 - )), - new Ortb2( - new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data) - ) - ); + 1)), + new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data))); } protected TargetingResult givenEmptyTargetingResult() { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 1dabeb1ad03..af4a809df78 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; @@ -12,7 +11,6 @@ import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -20,8 +18,6 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.JsonMerger; import java.util.List; @@ -32,16 +28,13 @@ @ExtendWith(MockitoExtension.class) public class OptableTargetingAuctionResponseHookTest extends BaseOptableTest { - @Mock - AuctionResponsePayload auctionResponsePayload; - @Mock(strictness = LENIENT) - AuctionInvocationContext invocationContext; - private AuctionResponseValidator auctionResponseValidator; - private final ObjectMapper mapper = new ObjectMapper(); - private final JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(mapper)); + private ConfigResolver configResolver; private AuctionResponseHook target; - private ConfigResolver configResolver; + @Mock + private AuctionResponsePayload auctionResponsePayload; + @Mock(strictness = LENIENT) + private AuctionInvocationContext invocationContext; @BeforeEach public void setUp() { @@ -66,8 +59,8 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); // when - final Future> future = target.call(auctionResponsePayload, - invocationContext); + final Future> future = + target.call(auctionResponsePayload, invocationContext); // then assertThat(future).isNotNull(); @@ -85,13 +78,17 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { @Test public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn() { // given - when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", - List.of(new AudienceId("audienceId")), "keyspace", 1)))); + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)))); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when - final Future> future = target.call(auctionResponsePayload, - invocationContext); + final Future> future = + target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); final BidResponse bidResponse = result .payloadUpdate() @@ -122,16 +119,16 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( @Test public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { // given - when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of(new Audience("provider", - List.of(new AudienceId("audienceId")), "keyspace", 1)))); - target = new OptableTargetingAuctionResponseHook( - configResolver, - mapper, - jsonMerger); + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)))); // when - final Future> future = target.call(auctionResponsePayload, - invocationContext); + final Future> future = + target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); // then diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index b96b4447000..f3463d677cf 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; -import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; @@ -22,8 +21,6 @@ import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import org.prebid.server.json.JacksonMapper; -import org.prebid.server.json.JsonMerger; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -34,36 +31,43 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { + private ConfigResolver configResolver; + @Mock - OptableTargeting optableTargeting; + private OptableTargeting optableTargeting; + @Mock - AuctionRequestPayload auctionRequestPayload; + private UserFpdActivityMask userFpdActivityMask; + + private OptableTargetingProcessedAuctionRequestHook target; + @Mock - AuctionInvocationContext invocationContext; + private AuctionRequestPayload auctionRequestPayload; + @Mock - Timeout timeout; + private AuctionInvocationContext invocationContext; + @Mock - UserFpdActivityMask userFpdActivityMask; + private ActivityInfrastructure activityInfrastructure; + @Mock - ActivityInfrastructure activityInfrastructure; - private ConfigResolver configResolver; - private JsonMerger jsonMerger = new JsonMerger(new JacksonMapper(new ObjectMapper())); - private OptableTargetingProcessedAuctionRequestHook target; + private Timeout timeout; @BeforeEach public void setUp() { - when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); - when(timeout.remaining()).thenReturn(1000L); - when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); - when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); - when(invocationContext.timeout()).thenReturn(timeout); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); target = new OptableTargetingProcessedAuctionRequestHook( configResolver, optableTargeting, userFpdActivityMask); + + when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); + when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); + when(invocationContext.timeout()).thenReturn(timeout); + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); + when(timeout.remaining()).thenReturn(1000L); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java index a46e59b1974..51c5c33890f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AuctionResponseValidatorTest.java @@ -68,8 +68,7 @@ public void shouldReturnSuccessStatus() { .returns(Reason.NONE, EnrichmentStatus::getReason); } - protected List givenTargeting() { - return List.of(new Audience("provider", List.of(new AudienceId("id")), - "keyspace", 1)); + private static List givenTargeting() { + return List.of(new Audience("provider", List.of(new AudienceId("id")), "keyspace", 1)); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java index 658f8c6214e..038a0958acc 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -72,14 +72,13 @@ public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { @Test public void shouldNotAddEidWhenSourceAlreadyPresent() { // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source", List.of(givenUid("id2", 3, null)), null))); + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( givenEid("source", List.of(givenUid("id", null, null)), null), - givenEid("source1", List.of(givenUid("id", null, null)), null) - )); + givenEid("source1", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); - final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source", List.of(givenUid("id2", 3, null)), null) - )); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) @@ -95,14 +94,13 @@ public void shouldNotAddEidWhenSourceAlreadyPresent() { @Test public void shouldAddEidWhenSourceIsNotAlreadyPresent() { // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source3", List.of(givenUid("id2", 3, null)), null))); + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( givenEid("source1", List.of(givenUid("id", null, null)), null), - givenEid("source2", List.of(givenUid("id", null, null)), null) - )); + givenEid("source2", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); - final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source3", List.of(givenUid("id2", 3, null)), null) - )); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) @@ -118,14 +116,13 @@ public void shouldAddEidWhenSourceIsNotAlreadyPresent() { @Test public void shouldNotMergeOriginEidsWithTheSameSource() { // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source3", List.of(givenUid("id2", 3, null)), null))); + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( givenEid("source", List.of(givenUid("id", null, null)), null), - givenEid("source", List.of(givenUid("id", null, null)), null) - )); + givenEid("source", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); - final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source3", List.of(givenUid("id2", 3, null)), null) - )); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) @@ -141,11 +138,11 @@ public void shouldNotMergeOriginEidsWithTheSameSource() { @Test public void shouldApplyOriginEidsWhenTargetingIsEmpty() { // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("source3", List.of(givenUid("id2", 3, null)), null))); + final BidRequest bidRequest = givenBidRequestWithUserEids(Collections.emptyList()); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); - final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source3", List.of(givenUid("id2", 3, null)), null) - )); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) @@ -161,12 +158,12 @@ public void shouldApplyOriginEidsWhenTargetingIsEmpty() { @Test public void shouldApplyTargetingEidsWhenOriginListIsEmpty() { // given + final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( givenEid("source", List.of(givenUid("id", null, null)), null), - givenEid("source1", List.of(givenUid("id", null, null)), null) - )); + givenEid("source1", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); - final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java index 74cc3668dff..8b2e4b14432 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidResponseEnricherTest.java @@ -56,13 +56,11 @@ public void shouldReturnOriginBidResponseWhenNoTargetingKeywords() { assertThat(targeting.get("keyspace")).isNull(); } - private List givenTargeting() { - return List.of( - new Audience("provider", - List.of( - new AudienceId("audienceId"), - new AudienceId("audienceId2")), - "keyspace", - 1)); + private static List givenTargeting() { + return List.of(new Audience( + "provider", + List.of(new AudienceId("audienceId"), new AudienceId("audienceId2")), + "keyspace", + 1)); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java index ccb6ac3fee5..1917fd6ca27 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -33,8 +33,9 @@ public class CacheTest { @Mock private PbcStorageService pbcStorageService; + @Spy - private JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper()); + private final JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper()); private final JacksonMapper mapper = new JacksonMapper(ObjectMapperProvider.mapper()); @@ -48,8 +49,8 @@ public void setUp() { @Test public void cacheShouldNotCallMapperIfNoEntry() { // given - when(pbcStorageService.retrieveEntry(any(), any(), any())).thenReturn( - Future.succeededFuture(ModuleCacheResponse.empty())); + when(pbcStorageService.retrieveEntry(any(), any(), any())) + .thenReturn(Future.succeededFuture(ModuleCacheResponse.empty())); // when final TargetingResult result = target.get("key").result(); @@ -63,8 +64,10 @@ public void cacheShouldNotCallMapperIfNoEntry() { public void cacheShouldReturnEntry() { // given final TargetingResult targetingResult = givenTargetingResult(); - when(pbcStorageService.retrieveEntry(any(), any(), any())).thenReturn( - Future.succeededFuture(ModuleCacheResponse.of("key", StorageDataType.TEXT, + when(pbcStorageService.retrieveEntry(any(), any(), any())) + .thenReturn(Future.succeededFuture(ModuleCacheResponse.of( + "key", + StorageDataType.TEXT, mapper.encodeToString(targetingResult)))); // when diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java similarity index 85% rename from extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java rename to extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java index a72d955a055..54b0d31d1a1 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java @@ -12,6 +12,7 @@ import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.request.ExtUser; import java.util.List; @@ -21,12 +22,13 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; -public class IdMapperTest { +public class IdsMapperTest { + + private final ObjectMapper objectMapper = ObjectMapperProvider.mapper(); - private final ObjectMapper objectMapper = new ObjectMapper(); private IdsMapper target; - private Map ppidMapping = Map.of("test.com", "c"); + private final Map ppidMapping = Map.of("test.com", "c"); @BeforeEach public void setUp() { @@ -36,25 +38,8 @@ public void setUp() { @Test public void shouldMapBidRequestToAllPossibleIds() { //given - final BidRequest bidRequest = givenBidRequest(builder -> { - final Map eids = Map.of("id5-sync.com", "id5_id", - "test.com", "test_id", "utiq.com", "utiq_id"); - - final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); - - builder.device(givenDevice()) - .user(givenUser(userBuilder -> { - final ExtUser extUser = ExtUser.builder().build(); - extUser.addProperty("optable", extUserOptable); - userBuilder.eids(toEids(eids)) - .ext(extUser) - .build(); - - return userBuilder; - })); - - return builder; - }); + final BidRequest bidRequest = givenBidRequestWithEids(Map.of("id5-sync.com", "id5_id", + "test.com", "test_id", "utiq.com", "utiq_id")); // when final List ids = target.toIds(bidRequest, ppidMapping); @@ -72,20 +57,6 @@ public void shouldMapBidRequestToAllPossibleIds() { .contains(Id.of("c", "test_id")); } - private User givenUser(UnaryOperator userCustomizer) { - return userCustomizer.apply(User.builder()).build(); - } - - private Device givenDevice() { - return Device.builder() - .ip("127.0.0.1") - .ipv6("0:0:0:0:0:0:0:1") - .lmt(0) - .os("android") - .ifa("ifa") - .build(); - } - @Test public void shouldMapNothing() { //given @@ -98,6 +69,25 @@ public void shouldMapNothing() { assertThat(ids).isNotNull(); } + private BidRequest givenBidRequestWithEids(Map eids) { + return givenBidRequest(builder -> { + final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); + + builder.device(givenDevice()) + .user(givenUser(userBuilder -> { + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", extUserOptable); + userBuilder.eids(toEids(eids)) + .ext(extUser) + .build(); + + return userBuilder; + })); + + return builder; + }); + } + private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { return bidRequestCustomizer.apply(BidRequest.builder() .id("requestId") @@ -116,13 +106,26 @@ private ExtUserOptable givenOptable() { .build(); } + private Device givenDevice() { + return Device.builder() + .ip("127.0.0.1") + .ipv6("0:0:0:0:0:0:0:1") + .lmt(0) + .os("android") + .ifa("ifa") + .build(); + } + + private User givenUser(UnaryOperator userCustomizer) { + return userCustomizer.apply(User.builder()).build(); + } + private List toEids(Map eids) { - return eids.entrySet().stream() + return eids.entrySet() + .stream() .map(it -> Eid.builder() .source(it.getKey()) - .uids(List.of(Uid.builder() - .id(it.getValue()) - .build())) + .uids(List.of(Uid.builder().id(it.getValue()).build())) .build()) .toList(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 8a8029c09f2..de2c01948fb 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -9,7 +9,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.geolocation.model.GeoInfo; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; @@ -34,9 +33,6 @@ public class OptableAttributesResolverTest extends BaseOptableTest { @Mock(strictness = LENIENT) private GppContext gppContext; - @Mock(strictness = LENIENT) - private GeoInfo geoInfo; - @Mock private OptableTargetingProperties properties; @@ -57,8 +53,8 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() { final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); // when - final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, - properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() @@ -78,8 +74,8 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); // when - final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, - properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() @@ -99,8 +95,8 @@ public void shouldResolveGppAttributes() { final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); // when - final OptableAttributes result = OptableAttributesResolver.resolveAttributes(auctionContext, - properties.getTimeout()); + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); // then assertThat(result).isNotNull() @@ -109,24 +105,11 @@ public void shouldResolveGppAttributes() { .returns(Set.of(1), OptableAttributes::getGppSid); } - public AuctionContext givenAuctionContext(TcfContext tcfContext) { - return AuctionContext.builder() - .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, - "8.8.8.8")).build(); - } - - public AuctionContext givenAuctionContext(GppContext gppContext) { - return AuctionContext.builder().gppContext(gppContext).build(); - } - public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { return AuctionContext.builder() .bidRequest(bidRequest) .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) - .gppContext(gppContext).build(); - } - - public AuctionContext givenAuctionContext(GeoInfo geoInfo) { - return AuctionContext.builder().geoInfo(geoInfo).build(); + .gppContext(gppContext) + .build(); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index e201891200f..9f793605f12 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -27,43 +27,37 @@ @ExtendWith(MockitoExtension.class) public class OptableTargetingTest extends BaseOptableTest { - @Mock(strictness = Mock.Strictness.LENIENT) - private Cache cache; - @Mock private IdsMapper idsMapper; - @Mock - private APIClientImpl apiClient; + @Mock(strictness = Mock.Strictness.LENIENT) + private Cache cache; @Mock - private Timeout timeout; - - private CachedAPIClient cachingAPIClient; + private APIClientImpl apiClient; private OptableTargeting target; - private OptableAttributes optableAttributes; - - private OptableTargetingProperties properties; + @Mock + private Timeout timeout; @BeforeEach public void setUp() { - optableAttributes = givenOptableAttributes(); - properties = givenOptableTargetingProperties(true); - cachingAPIClient = new CachedAPIClient(apiClient, cache, false); + final CachedAPIClient cachingAPIClient = new CachedAPIClient(apiClient, cache, false); target = new OptableTargeting(idsMapper, cachingAPIClient); } @Test public void shouldCallNonCachedAPIClient() { // given - final BidRequest bidRequest = givenBidRequest(); - properties = givenOptableTargetingProperties(false); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); + final BidRequest bidRequest = givenBidRequest(); + final OptableTargetingProperties properties = givenOptableTargetingProperties(false); + final OptableAttributes optableAttributes = givenOptableAttributes(); + // when final Future targetingResult = target.getTargeting( properties, bidRequest, optableAttributes, timeout); @@ -76,16 +70,17 @@ public void shouldCallNonCachedAPIClient() { @Test public void shouldUseCachedAPIClient() { // given - final BidRequest bidRequest = givenBidRequest(); - properties = givenOptableTargetingProperties(true); when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); when(cache.get(any())).thenReturn(Future.failedFuture(new NullPointerException())); when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); + final BidRequest bidRequest = givenBidRequest(); + final OptableTargetingProperties properties = givenOptableTargetingProperties(true); + final OptableAttributes optableAttributes = givenOptableAttributes(); + // when - final Future targetingResult = target.getTargeting( - properties, bidRequest, optableAttributes, timeout); + target.getTargeting(properties, bidRequest, optableAttributes, timeout); // then verify(cache).get(any()); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index b4ac5a740e4..2548a34dff3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; @@ -12,15 +11,9 @@ public class QueryBuilderTest { - private OptableAttributes optableAttributes; + private final OptableAttributes optableAttributes = givenOptableAttributes(); - private String idPrefixOrder; - - @BeforeEach - public void setUp() { - optableAttributes = givenOptableAttributes(); - idPrefixOrder = "c,c1"; - } + private final String idPrefixOrder = "c,c1"; @Test public void shouldSeparateAttributesFromIds() { @@ -75,7 +68,10 @@ public void shouldBuildQueryStringWithExtraAttributes() { @Test public void shouldBuildQueryStringWithRightOrder() { // given - final List ids = List.of(Id.of(Id.ID5, "ID5"), Id.of(Id.EMAIL, "email"), Id.of("c1", "123"), + final List ids = List.of( + Id.of(Id.ID5, "ID5"), + Id.of(Id.EMAIL, "email"), + Id.of("c1", "123"), Id.of("c", "234")); // when @@ -106,8 +102,7 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { // given final List ids = List.of(); - final OptableAttributes attributes = OptableAttributes.builder() - .build(); + final OptableAttributes attributes = OptableAttributes.builder().build(); // when final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java similarity index 69% rename from extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java rename to extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java index 3cf07190974..82bb1ca2e3a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java @@ -14,6 +14,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.json.JacksonMapper; +import org.prebid.server.util.HttpUtil; import org.prebid.server.vertx.httpclient.HttpClient; import java.util.List; @@ -25,29 +26,21 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -public class APIClientTest extends BaseOptableTest { - - private static final Double LOG_SAMPLING_RATE = 100.0; - - private static final String ACCEPT_HEADER = "Accept"; - - private static final String AUTHORIZATION_HEADER = "Authorization"; - - private static final String X_FORWARDED_FOR_HEADER = "X-forwarded-for"; +public class APIClientImplTest extends BaseOptableTest { @Mock private HttpClient httpClient; - @Mock - private Timeout timeout; + private final JacksonMapper jacksonMapper = new JacksonMapper(mapper); private APIClient target; - private final JacksonMapper jacksonMapper = new JacksonMapper(mapper); + @Mock + private Timeout timeout; @BeforeEach public void setUp() { - target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, LOG_SAMPLING_RATE); + target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, 100); } @Test @@ -57,8 +50,11 @@ public void shouldReturnTargetingResult() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("targeting_response.json"))); // when - final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + timeout); // then assertThat(result.result()).isNotNull(); @@ -75,8 +71,11 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { .thenReturn(Future.succeededFuture(givenFailHttpResponse("error_response.json"))); // when - final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + timeout); // then assertThat(result.result()).isNull(); @@ -89,8 +88,11 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { .thenReturn(Future.succeededFuture(givenSuccessHttpResponse("plain_text_response.json"))); // when - final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + timeout); // then assertThat(result.result()).isNull(); @@ -99,11 +101,15 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { @Test public void shouldNotFailWhenHttpClientIsCrashed() { // given - when(httpClient.get(any(), any(), anyLong())).thenThrow(new NullPointerException()); + when(httpClient.get(any(), any(), anyLong())) + .thenReturn(Future.failedFuture(new NullPointerException())); // when - final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + timeout); // then assertThat(result.result()).isNull(); @@ -112,13 +118,15 @@ public void shouldNotFailWhenHttpClientIsCrashed() { @Test public void shouldNotFailWhenInternalErrorOccurs() { // given - when(httpClient.get(any(), any(), anyLong())) - .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, - "plain_text_response.json"))); + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when - final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + List.of("8.8.8.8"), + timeout); // then assertThat(result.result()).isNull(); @@ -127,7 +135,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { @Test public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // given - target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, LOG_SAMPLING_RATE); + target = new APIClientImpl("http://endpoint.optable.com", httpClient, jacksonMapper, 10); when(httpClient.get(any(), any(), anyLong())) .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, @@ -140,17 +148,16 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); - assertThat(headersCaptor.getValue().get(ACCEPT_HEADER)).isEqualTo("application/json"); - assertThat(headersCaptor.getValue().get(AUTHORIZATION_HEADER)).isEqualTo("Bearer key"); + assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER)).isEqualTo("application/json"); + assertThat(headersCaptor.getValue().get(HttpUtil.AUTHORIZATION_HEADER)).isEqualTo("Bearer key"); assertThat(result.result()).isNull(); } @Test public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { // given - when(httpClient.get(any(), any(), anyLong())) - .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, - "plain_text_response.json"))); + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when final Future result = target.getTargeting( @@ -162,17 +169,16 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); - assertThat(headersCaptor.getValue().get(ACCEPT_HEADER)).isEqualTo("application/json"); - assertThat(headersCaptor.getValue().get(AUTHORIZATION_HEADER)).isNull(); + assertThat(headersCaptor.getValue().get(HttpUtil.ACCEPT_HEADER)).isEqualTo("application/json"); + assertThat(headersCaptor.getValue().get(HttpUtil.AUTHORIZATION_HEADER)).isNull(); assertThat(result.result()).isNull(); } @Test public void shouldPassThroughIpAddresses() { // given - when(httpClient.get(any(), any(), anyLong())) - .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, - "plain_text_response.json"))); + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when final Future result = target.getTargeting( @@ -184,25 +190,28 @@ public void shouldPassThroughIpAddresses() { // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); - assertThat(headersCaptor.getValue().getAll(X_FORWARDED_FOR_HEADER)).contains("8.8.8.8", "2001:4860:4860::8888"); + assertThat(headersCaptor.getValue().getAll(HttpUtil.X_FORWARDED_FOR_HEADER)) + .contains("8.8.8.8", "2001:4860:4860::8888"); assertThat(result.result()).isNull(); } @Test public void shouldNotPassThroughIpAddressWhenNotSpecified() { // given - when(httpClient.get(any(), any(), anyLong())) - .thenReturn(Future.succeededFuture(givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, - "plain_text_response.json"))); + when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( + givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); // when - final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), null, timeout); + final Future result = target.getTargeting( + givenOptableTargetingProperties(false), + givenQuery(), + null, + timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); - assertThat(headersCaptor.getValue().get(X_FORWARDED_FOR_HEADER)).isNull(); + assertThat(headersCaptor.getValue().get(HttpUtil.X_FORWARDED_FOR_HEADER)).isNull(); assertThat(result.result()).isNull(); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java index 5b04f881579..6c365f2c2c0 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -8,7 +8,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.Query; -import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; @@ -27,22 +26,19 @@ @ExtendWith(MockitoExtension.class) public class CachedAPIClientTest extends BaseOptableTest { - @Mock(strictness = Mock.Strictness.LENIENT) - private Cache cache; - @Mock private APIClientImpl apiClient; @Mock(strictness = Mock.Strictness.LENIENT) - private Timeout timeout; + private Cache cache; - private APIClient target; + private CachedAPIClient target; - private OptableTargetingProperties properties; + @Mock(strictness = Mock.Strictness.LENIENT) + private Timeout timeout; @BeforeEach public void setUp() { - properties = givenOptableTargetingProperties(true); target = new CachedAPIClient(apiClient, cache, false); when(timeout.remaining()).thenReturn(1000L); } @@ -58,7 +54,10 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), timeout); + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + timeout); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -81,7 +80,10 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), timeout); + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + timeout); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -102,7 +104,10 @@ public void shouldUseCachedResult() { // when final Future targetingResult = target.getTargeting( - properties, query, List.of("8.8.8.8"), timeout); + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + timeout); // then final User user = targetingResult.result().getOrtb2().getUser(); @@ -119,15 +124,17 @@ public void shouldUseCachedResult() { @Test public void shouldNotFailWhenApiClientIsFailed() { // given - properties = givenOptableTargetingProperties(true); final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); when(apiClient.getTargeting(any(), any(), any(), any())) .thenReturn(Future.failedFuture(new NullPointerException())); // when - final Future targetingResult = target.getTargeting(properties, query, - List.of("8.8.8.8"), timeout); + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + timeout); // then assertThat(targetingResult.result()).isNull(); @@ -137,7 +144,6 @@ public void shouldNotFailWhenApiClientIsFailed() { @Test public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { // given - properties = givenOptableTargetingProperties(true); final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); when(apiClient.getTargeting(any(), any(), any(), any())) @@ -146,8 +152,11 @@ public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { // when target = new CachedAPIClient(apiClient, cache, true); - final Future targetingResult = target.getTargeting(properties, query, - List.of("8.8.8.8"), timeout); + final Future targetingResult = target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + timeout); // then final TargetingResult result = targetingResult.result(); From f21dddb0983ee4f96bd798a059aff4dcaae3c9d1 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 14 Jul 2025 15:07:51 +0200 Subject: [PATCH 39/41] optable-targeting: Up PBS version --- extra/modules/optable-targeting/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index 49e9ca94f9c..7b5b1727fc9 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -5,7 +5,7 @@ org.prebid.server.hooks.modules all-modules - 3.28.0-SNAPSHOT + 3.29.0-SNAPSHOT optable-targeting From c2c2ab8f004d4cb569ec3c475e525ca19e748a91 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 14 Jul 2025 15:15:46 +0200 Subject: [PATCH 40/41] optable-targeting: Remove redundant test --- .../v1/OptableTargetingModuleTest.java | 69 ------------------- 1 file changed, 69 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java deleted file mode 100644 index dfe98da2245..00000000000 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingModuleTest.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; -import org.prebid.server.hooks.v1.Hook; -import org.prebid.server.hooks.v1.InvocationContext; -import org.prebid.server.hooks.v1.Module; - -import java.util.Collection; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.Mock.Strictness.LENIENT; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) -public class OptableTargetingModuleTest extends BaseOptableTest { - - @Mock - ConfigResolver configResolver; - - @Mock - OptableTargeting optableTargeting; - - @Mock(strictness = LENIENT) - UserFpdActivityMask userFpdActivityMask; - - @Test - public void shouldReturnNonBlankCode() { - // given - final Module module = new OptableTargetingModule(null); - - // when and then - assertThat(module.code()) - .isNotBlank() - .isEqualTo("optable-targeting"); - - } - - @Test - public void shouldReturnHooks() { - // given - when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) - .thenAnswer(answer -> answer.getArgument(0)); - final Collection> hooks = - List.of(new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask), - new OptableTargetingAuctionResponseHook( - configResolver, - mapper, - jsonMerger)); - - final Module module = new OptableTargetingModule(hooks); - - // when and then - assertThat(module.hooks()) - .hasSize(2) - .isEqualTo(hooks); - } -} From 10f9d34628e8b4e3985523665fc69c081cd92759 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Tue, 15 Jul 2025 14:15:00 +0200 Subject: [PATCH 41/41] optable-targeting: Code cleanup --- .../targeting/v1/core/IdsMapperTest.java | 28 +++++++------------ .../targeting/v1/core/QueryBuilderTest.java | 2 -- 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java index 54b0d31d1a1..39693661629 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java @@ -38,8 +38,10 @@ public void setUp() { @Test public void shouldMapBidRequestToAllPossibleIds() { //given - final BidRequest bidRequest = givenBidRequestWithEids(Map.of("id5-sync.com", "id5_id", - "test.com", "test_id", "utiq.com", "utiq_id")); + final BidRequest bidRequest = givenBidRequestWithEids(Map.of( + "id5-sync.com", "id5_id", + "test.com", "test_id", + "utiq.com", "utiq_id")); // when final List ids = target.toIds(bidRequest, ppidMapping); @@ -70,22 +72,12 @@ public void shouldMapNothing() { } private BidRequest givenBidRequestWithEids(Map eids) { - return givenBidRequest(builder -> { - final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); - - builder.device(givenDevice()) - .user(givenUser(userBuilder -> { - final ExtUser extUser = ExtUser.builder().build(); - extUser.addProperty("optable", extUserOptable); - userBuilder.eids(toEids(eids)) - .ext(extUser) - .build(); - - return userBuilder; - })); - - return builder; - }); + final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", extUserOptable); + + final User user = givenUser(userBuilder -> userBuilder.eids(toEids(eids)).ext(extUser)); + return givenBidRequest(builder -> builder.device(givenDevice()).user(user)); } private static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 2548a34dff3..e7212fb5884 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -93,7 +93,6 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); // then - assertThat(query).isNotNull(); assertThat(query.toQueryString()).isEqualTo("gdpr=0"); } @@ -108,7 +107,6 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); // then - assertThat(query).isNull(); }