From 34d95a6005cb9a0219ca39a383775875959547ee Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 4 May 2026 19:13:16 +0200 Subject: [PATCH 01/19] optable-targeting: implement Non-Blocking Early Network Call --- extra/modules/optable-targeting/pom.xml | 8 ++ .../config/OptableTargetingConfig.java | 17 ++- .../targeting/model/ModuleContext.java | 13 ++ .../optable/targeting/v1/OptableHook.java | 37 +++++ .../v1/OptableRawAuctionRequestHook.java | 87 ++++++++++++ ...eTargetingProcessedAuctionRequestHook.java | 119 +++++----------- .../targeting/v1/core/NetworkCall.java | 88 ++++++++++++ .../v1/core/OptableAttributesResolver.java | 32 ++++- .../optable/targeting/v1/BaseOptableTest.java | 12 ++ .../v1/OptableRawAuctionRequestHookTest.java | 127 ++++++++++++++++++ ...getingProcessedAuctionRequestHookTest.java | 64 ++++++--- .../core/OptableAttributesResolverTest.java | 59 +++++++- .../prebid-config-with-optable-old.yaml | 53 ++++++++ .../sample-app-settings-optable-old.yaml | 63 +++++++++ .../configs/sample-app-settings-optable.yaml | 13 ++ 15 files changed, 674 insertions(+), 118 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java create mode 100644 sample/configs/prebid-config-with-optable-old.yaml create mode 100644 sample/configs/sample-app-settings-optable-old.yaml diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index e202d7cfdd6..4853322dc0b 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -12,4 +12,12 @@ optable-targeting Optable targeting module + + + + io.vertx + vertx-junit5 + test + + diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 91afba88a31..117b101e096 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 @@ -4,12 +4,14 @@ 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.OptableRawAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.core.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.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; @@ -86,18 +88,25 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties); } + @Bean + NetworkCall networkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) { + return new NetworkCall(optableTargeting, userFpdActivityMask); + } + @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, - OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, + NetworkCall networkCall, JsonMerger jsonMerger, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { return new OptableTargetingModule(List.of( + new OptableRawAuctionRequestHook( + configResolver, + networkCall, + logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, - optableTargeting, - userFpdActivityMask, + networkCall, logSamplingRate), new OptableTargetingAuctionResponseHook( configResolver, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index ed0264f0249..fc4122b7d48 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,7 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.model; +import io.vertx.core.Future; import lombok.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import java.util.List; @@ -19,8 +21,19 @@ public class ModuleContext { private long optableTargetingExecutionTime; + private boolean isEarlyNetworkCallEnabled = false; + + private Future optableTargetingCall; + + private long callTargetingAPITimestamp; + public static ModuleContext of(AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); return moduleContext != null ? moduleContext : new ModuleContext(); } + + public void failWithExecutionTime(long executionTime) { + setOptableTargetingExecutionTime(executionTime); + setEnrichRequestStatus(EnrichmentStatus.failure()); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java new file mode 100644 index 00000000000..71fd09a7c0d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; +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.AuctionRequestPayload; + +public class OptableHook { + + private OptableHook() { + } + + public static boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { + return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); + } + + public static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java new file mode 100644 index 00000000000..a96a863ba01 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -0,0 +1,87 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.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.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; +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.RawAuctionRequestHook; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; + +public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); + + private static final String CODE = "optable-targeting-raw-auction-request-hook"; + + private final ConfigResolver configResolver; + private final NetworkCall networkCall; + private final double logSamplingRate; + + public OptableRawAuctionRequestHook(ConfigResolver configResolver, + NetworkCall networkCall, + double logSamplingRate) { + + this.configResolver = Objects.requireNonNull(configResolver); + this.networkCall = Objects.requireNonNull(networkCall); + this.logSamplingRate = logSamplingRate; + } + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext) { + + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyNetworkCallEnabled(true); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + + if (!OptableHook.isTargetingPropertiesValid(properties)) { + conditionalLogger.error( + "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); + + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + + return OptableHook.update(BidRequestCleaner.instance(), moduleContext); + } + + final Future optableTargetingCall = networkCall.makeRequest( + payload, + invocationContext, + properties); + + moduleContext.setOptableTargetingCall(optableTargetingCall); + + return updateModuleContext(moduleContext); + } + + private static Future> updateModuleContext(ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .moduleContext(moduleContext) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index a5ad2559d40..80792de93ba 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -1,31 +1,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.apache.commons.lang3.StringUtils; -import org.prebid.server.activity.Activity; -import org.prebid.server.activity.ComponentType; -import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; -import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; -import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; -import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; -import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -41,23 +26,22 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( - LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); + LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); public static final String CODE = "optable-targeting-processed-auction-request-hook"; + private static final String AUCTION_NOT_PROPERLY_CONFIGURED = + "Account not properly configured: tenant and/or origin is missing."; + private final ConfigResolver configResolver; - private final OptableTargeting optableTargeting; - private final UserFpdActivityMask userFpdActivityMask; + private final NetworkCall networkCall; private final double logSamplingRate; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, - OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, + NetworkCall networkCall, double logSamplingRate) { - this.configResolver = Objects.requireNonNull(configResolver); - this.optableTargeting = Objects.requireNonNull(optableTargeting); - this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.networkCall = Objects.requireNonNull(networkCall); this.logSamplingRate = logSamplingRate; } @@ -66,80 +50,51 @@ public Future> call(AuctionRequestPayloa AuctionInvocationContext invocationContext) { final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); - final ModuleContext moduleContext = new ModuleContext(); - final long callTargetingAPITimestamp = System.currentTimeMillis(); + final ModuleContext moduleContext = ModuleContext.of(invocationContext); - if (!isTargetingPropertiesValid(properties)) { - conditionalLogger.error( - "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); + final Future optableTargetingCall = moduleContext.isEarlyNetworkCallEnabled() + ? moduleContext.getOptableTargetingCall() + : makeOptableTargetingCall(auctionRequestPayload, invocationContext, moduleContext, properties); - moduleContext.setOptableTargetingExecutionTime(System.currentTimeMillis() - callTargetingAPITimestamp); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + if (optableTargetingCall == null) { + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); return update(BidRequestCleaner.instance(), moduleContext); } - final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); - - final Timeout timeout = getHookTimeout(invocationContext); - final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( - invocationContext.auctionContext(), - properties.getTimeout()); - - return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) + final Future> future = optableTargetingCall .compose(targetingResult -> { moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - callTargetingAPITimestamp); + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); return enrichedPayload(targetingResult, moduleContext, properties); }) .recover(throwable -> { - moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - callTargetingAPITimestamp); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); return update(BidRequestCleaner.instance(), moduleContext); }); - } - private boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { - return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); + return future; } - private BidRequest applyActivityRestrictions(BidRequest bidRequest, - AuctionInvocationContext auctionInvocationContext) { - - final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); - final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( - ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), - bidRequest); - final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); - - final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( - Activity.TRANSMIT_UFPD, activityInvocationPayload); - final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( - Activity.TRANSMIT_EIDS, activityInvocationPayload); - final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( - Activity.TRANSMIT_GEO, activityInvocationPayload); - - return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); - } - - private BidRequest maskUserPersonalInfo(BidRequest bidRequest, - boolean disallowTransmitUfpd, - boolean disallowTransmitEids, - boolean disallowTransmitGeo) { - - final User maskedUser = userFpdActivityMask.maskUser( - bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); - final Device maskedDevice = userFpdActivityMask.maskDevice( - bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); - - return bidRequest.toBuilder() - .user(maskedUser) - .device(maskedDevice) - .build(); - } + private Future makeOptableTargetingCall( + AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + if (!OptableHook.isTargetingPropertiesValid(properties)) { + conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate); + + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + return Future.failedFuture(AUCTION_NOT_PROPERLY_CONFIGURED); + } - private Timeout getHookTimeout(AuctionInvocationContext invocationContext) { - return invocationContext.timeout(); + return networkCall.makeRequest( + payload, + invocationContext, + properties); } private Future> enrichedPayload(TargetingResult targetingResult, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java new file mode 100644 index 00000000000..be7d2c42acb --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java @@ -0,0 +1,88 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; +import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.Objects; + +public class NetworkCall { + + private final OptableTargeting optableTargeting; + private final UserFpdActivityMask userFpdActivityMask; + + public NetworkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) { + + this.optableTargeting = Objects.requireNonNull(optableTargeting); + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + } + + public Future makeRequest(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties) { + + final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext); + + final Timeout timeout = getHookTimeout(invocationContext); + final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( + invocationContext.auctionContext(), + properties.getTimeout()); + + return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout); + } + + private static Timeout getHookTimeout(AuctionInvocationContext invocationContext) { + return invocationContext.timeout(); + } + + private BidRequest applyActivityRestrictions(BidRequest bidRequest, + AuctionInvocationContext auctionInvocationContext) { + + final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), + bidRequest); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_UFPD, activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_EIDS, activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_GEO, activityInvocationPayload); + + return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); + } + + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, + boolean disallowTransmitUfpd, + boolean disallowTransmitEids, + boolean disallowTransmitGeo) { + + final User maskedUser = userFpdActivityMask.maskUser( + bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); + final Device maskedDevice = userFpdActivityMask.maskDevice( + bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); + + return bidRequest.toBuilder() + .user(maskedUser) + .device(maskedDevice) + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 7f2aad0657d..8009a9f2ff5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,11 +1,16 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; -import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; import java.util.ArrayList; import java.util.List; @@ -17,18 +22,33 @@ private OptableAttributesResolver() { } public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { - final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext(); final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Optional regs = Optional.ofNullable(bidRequest.getRegs()); + final Integer gdpr = regs + .map(Regs::getGdpr) + .orElseGet(() -> regs.map(Regs::getExt) + .map(ExtRegs::getGdpr) + .orElse(null)); + final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() .ips(resolveIp(auctionContext)) .userAgent(resolveUserAgent(auctionContext)) .timeout(timeout); - if (tcfContext.isConsentValid()) { - builder - .gdprApplies(tcfContext.isInGdprScope()) - .gdprConsent(tcfContext.getConsentString()); + if (gdpr != null && gdpr > 0) { + final Optional user = Optional.ofNullable(bidRequest.getUser()); + final String consent = user.map(User::getConsent) + .orElseGet(() -> user.map(User::getExt) + .map(ExtUser::getConsent) + .orElse(null)); + + if (StringUtils.isNotEmpty(consent)) { + builder + .gdprApplies(true) + .gdprConsent(consent); + } } if (gppScope.getGppModel() != null) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 99f24ea4bc5..90f2008cec7 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 @@ -103,6 +103,14 @@ protected BidRequest givenBidRequestWithUserEids(List eids) { .build(); } + protected BidRequest givenBidRequestWithUser(User user) { + return BidRequest.builder() + .user(user) + .device(givenDevice()) + .cur(List.of("USD")) + .build(); + } + protected BidRequest givenBidRequestWithUserData(List data) { return BidRequest.builder() .user(givenUserWithData(data)) @@ -254,4 +262,8 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, protected Query givenQuery() { return Query.of("?que", "ry"); } + + protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { + return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled)); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java new file mode 100644 index 00000000000..00a0bbf77ee --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -0,0 +1,127 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +@MockitoSettings(strictness = Strictness.LENIENT) +@ExtendWith(VertxExtension.class) +public class OptableRawAuctionRequestHookTest extends BaseOptableTest { + + @Mock + private OptableTargeting optableTargeting; + @Mock + private UserFpdActivityMask userFpdActivityMask; + @Mock + private AuctionRequestPayload auctionRequestPayload; + @Mock + private ActivityInfrastructure activityInfrastructure; + @Mock + private AuctionInvocationContext invocationContext; + @Mock + private Timeout timeout; + + private ConfigResolver configResolver; + private NetworkCall networkCall; + private OptableRawAuctionRequestHook target; + + @BeforeEach + public void setUp() { + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, 0.01); + when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); + when(invocationContext.timeout()).thenReturn(timeout); + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); + when(timeout.remaining()).thenReturn(1000L); + } + + @Test + public void shouldHaveRightCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-raw-auction-request-hook"); + } + + @SneakyThrows + @Test + public void shouldInjectEarlyNetworkCallToModuleContext(VertxTestContext vertxTestContext) { + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", "origin", true)); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> result = + target.call(auctionRequestPayload, invocationContext); + + // then + assertThat(result).isNotNull(); + result.map(res -> (ModuleContext) res.moduleContext()) + .compose(ModuleContext::getOptableTargetingCall) + .onComplete(call -> { + vertxTestContext.verify(() -> { + assertThat(call.result()).isNotNull(); + }); + vertxTestContext.completeNow(); + }); + } + + @SneakyThrows + @Test + public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAccountConfiguration( + VertxTestContext vertxTestContext) { + + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", null, true)); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + configResolver = new ConfigResolver( + mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true)); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, 0.01); + + // when + final Future> result = + target.call(auctionRequestPayload, invocationContext); + + // then + assertThat(result).isNotNull(); + result.map(res -> (ModuleContext) res.moduleContext()) + .onComplete(cxt -> { + vertxTestContext.verify(() -> { + final ModuleContext moduleContext = cxt.result(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.isEarlyNetworkCallEnabled()).isTrue(); + }); + vertxTestContext.completeNow(); + }); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 008262b8a3e..190004eb758 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 @@ -17,6 +17,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -41,8 +42,6 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock private UserFpdActivityMask userFpdActivityMask; - private OptableTargetingProcessedAuctionRequestHook target; - @Mock private AuctionRequestPayload auctionRequestPayload; @@ -55,16 +54,17 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock private Timeout timeout; + private NetworkCall networkCall; + + private OptableTargetingProcessedAuctionRequestHook target; + @BeforeEach public void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); @@ -131,6 +131,40 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); } + @Test + public void shouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyNetworkCallEnabled(true); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + moduleContext.setOptableTargetingCall( + networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( + "key", "tenant", "origin", false))); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + 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 shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { // given @@ -138,11 +172,7 @@ public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); - target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -170,11 +200,7 @@ public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { mapper, jsonMerger, givenOptableTargetingProperties("key", null, "origin", false)); - target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); @@ -249,8 +275,4 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() private ObjectNode givenAccountConfig(boolean cacheEnabled) { return givenAccountConfig("key", "tenant", "origin", cacheEnabled); } - - private ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { - return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled)); - } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index de2c01948fb..9621758cab0 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -2,6 +2,8 @@ import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,6 +17,8 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.Privacy; import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; import java.util.List; import java.util.Set; @@ -42,15 +46,32 @@ public void setUp() { } @Test - public void shouldResolveTcfAttributesWhenConsentIsValid() { + public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() { // given final GppModel gppModel = mock(); - when(tcfContext.isConsentValid()).thenReturn(true); - when(tcfContext.isInGdprScope()).thenReturn(true); - when(tcfContext.getConsentString()).thenReturn("consent"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); + final AuctionContext auctionContext = + givenAuctionContext(givenBidRequestWithGdprORTB26(true, "consent"), tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(true, OptableAttributes::isGdprApplies) + .returns("consent", OptableAttributes::getGdprConsent); + } + + @Test + public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { + // given + final GppModel gppModel = mock(); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = + givenAuctionContext(givenBidRequestWithGdprORTB25(true, "consent"), tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( @@ -62,6 +83,34 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() { .returns("consent", OptableAttributes::getGdprConsent); } + private BidRequest givenBidRequestWithGdprORTB26(boolean isGdprEnabled, String consent) { + final User user = User.builder() + .consent(consent) + .build(); + + return BidRequest.builder() + .user(user) + .regs(Regs.builder() + .gdpr(isGdprEnabled ? 1 : 0) + .build()) + .build(); + } + + private BidRequest givenBidRequestWithGdprORTB25(boolean isGdprEnabled, String consent) { + final User user = User.builder() + .ext(ExtUser.builder() + .consent(consent) + .build()) + .build(); + + return BidRequest.builder() + .user(user) + .regs(Regs.builder() + .ext(ExtRegs.of(isGdprEnabled ? 1 : 0, null, null, null)) + .build()) + .build(); + } + @Test public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // given diff --git a/sample/configs/prebid-config-with-optable-old.yaml b/sample/configs/prebid-config-with-optable-old.yaml new file mode 100644 index 00000000000..9efe5a34e5c --- /dev/null +++ b/sample/configs/prebid-config-with-optable-old.yaml @@ -0,0 +1,53 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true + improvedigital: + enabled: true + colossus: + enabled: true + triplelift: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/configs/sample-app-settings-optable-old.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample/stored + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + optable-targeting: + enabled: true + modules: + optable-targeting: + api-endpoint: https://na.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}} diff --git a/sample/configs/sample-app-settings-optable-old.yaml b/sample/configs/sample-app-settings-optable-old.yaml new file mode 100644 index 00000000000..7a533da3697 --- /dev/null +++ b/sample/configs/sample-app-settings-optable-old.yaml @@ -0,0 +1,63 @@ +accounts: + - id: 1 + status: active + auction: + price-granularity: low + privacy: + ccpa: + enabled: true + gdpr: + enabled: true + cookie-sync: + default-limit: 8 + max-limit: 15 + coop-sync: + default: true + analytics: + allow-client-details: true + hooks: + modules: + optable-targeting: + api-key: key + tenant: optable + origin: web-sdk-demo + 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" + } + ] + } + ] + } + } + } + } + } diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 7a533da3697..571ad0a5a97 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -31,6 +31,19 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { + "raw-auction-request": { + "groups": [ + { + "timeout": 1000, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, "processed-auction-request": { "groups": [ { From 97fa4a987e0066f856abd497076cfe7c7f169e74 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 11 May 2026 21:22:08 +0200 Subject: [PATCH 02/19] optable-targeting: implement per-bidder enrichment percentage --- .../config/OptableTargetingConfig.java | 7 + .../targeting/model/ModuleContext.java | 10 + .../config/OptableTargetingProperties.java | 10 + .../v1/OptableBidderRequestHook.java | 86 ++++++++ .../optable/targeting/v1/OptableHook.java | 7 +- .../v1/OptableRawAuctionRequestHook.java | 15 ++ ...eTargetingProcessedAuctionRequestHook.java | 73 ++++--- .../targeting/v1/core/AliasesResolver.java | 27 +++ .../targeting/v1/core/BidRequestEnricher.java | 184 +---------------- .../v1/core/BidderEnrichmentDicer.java | 61 ++++++ .../v1/core/BidderRequestEnricher.java | 23 +++ .../targeting/v1/core/RequestEnricher.java | 191 ++++++++++++++++++ .../v1/OptableRawAuctionRequestHookTest.java | 10 +- .../configs/sample-app-settings-optable.yaml | 19 ++ 14 files changed, 509 insertions(+), 214 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.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 117b101e096..0d10536ffa6 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,12 +2,16 @@ import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cache.PbcStorageService; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AliasesResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentDicer; 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; @@ -97,17 +101,20 @@ NetworkCall networkCall(OptableTargeting optableTargeting, UserFpdActivityMask u OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, NetworkCall networkCall, JsonMerger jsonMerger, + BidderCatalog bidderCatalog, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { return new OptableTargetingModule(List.of( new OptableRawAuctionRequestHook( configResolver, networkCall, + BidderEnrichmentDicer.of(AliasesResolver.of(bidderCatalog)), logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, networkCall, logSamplingRate), + new OptableBidderRequestHook(), new OptableTargetingAuctionResponseHook( configResolver, ObjectMapperProvider.mapper(), diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index fc4122b7d48..c88b88f5e1b 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 @@ -2,11 +2,13 @@ import io.vertx.core.Future; import lombok.Data; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import java.util.List; +import java.util.Set; @Data public class ModuleContext { @@ -27,6 +29,10 @@ public class ModuleContext { private long callTargetingAPITimestamp; + private Set biddersToEnrich; + + private OptableTargetingProperties optableTargetingProperties; + public static ModuleContext of(AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); return moduleContext != null ? moduleContext : new ModuleContext(); @@ -36,4 +42,8 @@ public void failWithExecutionTime(long executionTime) { setOptableTargetingExecutionTime(executionTime); setEnrichRequestStatus(EnrichmentStatus.failure()); } + + public boolean hasOptableTargetingProperties() { + return optableTargetingProperties != null; + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 7f0598da83e..0fe5eff4b5b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; +import org.apache.commons.collections.MapUtils; import java.util.Map; import java.util.Set; @@ -42,4 +43,13 @@ public final class OptableTargetingProperties { Set optableInserterEidsIgnore = Set.of(); CacheProperties cache = new CacheProperties(); + + Integer enrichmentPercentage = 100; + + @JsonProperty("bidder-enrichment-percentages") + Map bidderEnrichmentPercentages = Map.of(); + + public boolean isPerBidderEnrichmentEnabled() { + return enrichmentPercentage != 100 || MapUtils.isNotEmpty(bidderEnrichmentPercentages); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java new file mode 100644 index 00000000000..e7f741976ad --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java @@ -0,0 +1,86 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderRequestEnricher; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestHook; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +import java.util.Set; + +public class OptableBidderRequestHook implements BidderRequestHook { + + public static final String CODE = "optable-targeting-bidder-request-hook"; + + @Override + public Future> call(BidderRequestPayload bidderRequestPayload, + BidderInvocationContext invocationContext) { + + final ModuleContext moduleContext = ModuleContext.of(invocationContext); + final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties(); + if (!properties.isPerBidderEnrichmentEnabled()) { + return noAction(moduleContext); + } + + final Set biddersToEnrich = moduleContext.getBiddersToEnrich(); + if (CollectionUtils.isEmpty(biddersToEnrich)) { + return noAction(moduleContext); + } + + return moduleContext.getOptableTargetingCall() + .compose(targetingResult -> + enrichedPayload( + targetingResult, moduleContext, moduleContext.getOptableTargetingProperties())) + .recover(throwable -> noAction(moduleContext)); + } + + private Future> enrichedPayload(TargetingResult targetingResult, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + + return update(BidderRequestEnricher.of(targetingResult, properties), moduleContext); + } + + private Future> noAction(ModuleContext moduleContext) { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .moduleContext(moduleContext) + .build()); + } + + private static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java index 71fd09a7c0d..fa22ad5c3df 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java @@ -10,7 +10,6 @@ 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.AuctionRequestPayload; public class OptableHook { @@ -21,12 +20,12 @@ public static boolean isTargetingPropertiesValid(OptableTargetingProperties prop return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); } - public static Future> update( - PayloadUpdate payloadUpdate, + public static Future> update( + PayloadUpdate payloadUpdate, ModuleContext moduleContext) { return Future.succeededFuture( - InvocationResultImpl.builder() + InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index a96a863ba01..eaa1561f23c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -1,12 +1,15 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; +import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentDicer; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.v1.InvocationAction; @@ -19,6 +22,7 @@ import org.prebid.server.log.LoggerFactory; import java.util.Objects; +import java.util.Set; public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { @@ -29,14 +33,17 @@ public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { private final ConfigResolver configResolver; private final NetworkCall networkCall; + private final BidderEnrichmentDicer bidderEnrichmentDicer; private final double logSamplingRate; public OptableRawAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, + BidderEnrichmentDicer bidderEnrichmentDicer, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); this.networkCall = Objects.requireNonNull(networkCall); + this.bidderEnrichmentDicer = Objects.requireNonNull(bidderEnrichmentDicer); this.logSamplingRate = logSamplingRate; } @@ -48,6 +55,7 @@ public Future> call(AuctionRequestPayloa final ModuleContext moduleContext = new ModuleContext(); moduleContext.setEarlyNetworkCallEnabled(true); moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + moduleContext.setOptableTargetingProperties(properties); if (!OptableHook.isTargetingPropertiesValid(properties)) { conditionalLogger.error( @@ -59,6 +67,13 @@ public Future> call(AuctionRequestPayloa return OptableHook.update(BidRequestCleaner.instance(), moduleContext); } + final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest(); + final Set biddersToEnrich = bidderEnrichmentDicer.dice(bidRequest, properties); + if (CollectionUtils.isEmpty(biddersToEnrich)) { + return OptableHook.update(BidRequestCleaner.instance(), moduleContext); + } + + moduleContext.setBiddersToEnrich(biddersToEnrich); final Future optableTargetingCall = networkCall.makeRequest( payload, 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 80792de93ba..c506ad03b90 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 @@ -40,6 +40,7 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, double logSamplingRate) { + this.configResolver = Objects.requireNonNull(configResolver); this.networkCall = Objects.requireNonNull(networkCall); this.logSamplingRate = logSamplingRate; @@ -49,39 +50,72 @@ public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver public Future> call(AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); final ModuleContext moduleContext = ModuleContext.of(invocationContext); + final OptableTargetingProperties properties = + resolveOptableTargetingProperties(moduleContext, invocationContext); final Future optableTargetingCall = moduleContext.isEarlyNetworkCallEnabled() - ? moduleContext.getOptableTargetingCall() - : makeOptableTargetingCall(auctionRequestPayload, invocationContext, moduleContext, properties); + ? resolveEarlyNetworkCall(moduleContext) + : resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties); if (optableTargetingCall == null) { - moduleContext.failWithExecutionTime( - System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext)); return update(BidRequestCleaner.instance(), moduleContext); } - final Future> future = optableTargetingCall + return optableTargetingCall .compose(targetingResult -> { - moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); - return enrichedPayload(targetingResult, moduleContext, properties); + moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); + return enrichPayload( + properties.isPerBidderEnrichmentEnabled(), targetingResult, moduleContext, properties); }) .recover(throwable -> { - moduleContext.failWithExecutionTime( - System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext)); return update(BidRequestCleaner.instance(), moduleContext); }); + } + + private Future> enrichPayload( + boolean perBidderEnrichmentEnabled, + TargetingResult targetingResult, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + + final PayloadUpdate payloadUpdate = perBidderEnrichmentEnabled + ? BidRequestCleaner.instance() + : BidRequestCleaner.instance().andThen(BidRequestEnricher.of(targetingResult, properties))::apply; + + return update(payloadUpdate, moduleContext); + } - return future; + private Future resolveEarlyNetworkCall(ModuleContext moduleContext) { + return moduleContext.getOptableTargetingCall(); } - private Future makeOptableTargetingCall( + private static long calcAPICallExecutionTime(ModuleContext moduleContext) { + return System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp(); + } + + private OptableTargetingProperties resolveOptableTargetingProperties(ModuleContext moduleContext, + AuctionInvocationContext invocationContext) { + + final OptableTargetingProperties properties = moduleContext.hasOptableTargetingProperties() + ? moduleContext.getOptableTargetingProperties() + : configResolver.resolve(invocationContext.accountConfig()); + moduleContext.setOptableTargetingProperties(properties); + + return properties; + } + + private Future resolvePreEarlyNetworkCall( AuctionRequestPayload payload, AuctionInvocationContext invocationContext, ModuleContext moduleContext, OptableTargetingProperties properties) { + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); if (!OptableHook.isTargetingPropertiesValid(properties)) { conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate); @@ -97,19 +131,6 @@ private Future makeOptableTargetingCall( properties); } - private Future> enrichedPayload(TargetingResult targetingResult, - ModuleContext moduleContext, - OptableTargetingProperties properties) { - - moduleContext.setTargeting(targetingResult.getAudience()); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - return update( - BidRequestCleaner.instance() - .andThen(BidRequestEnricher.of(targetingResult, properties)) - ::apply, - moduleContext); - } - private static Future> update( PayloadUpdate payloadUpdate, ModuleContext moduleContext) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java new file mode 100644 index 00000000000..f86724de969 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolver.java @@ -0,0 +1,27 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import lombok.AllArgsConstructor; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.util.PbsUtil; + +import java.util.Map; +import java.util.Optional; + +@AllArgsConstructor(staticName = "of") +public class AliasesResolver { + + private final BidderCatalog bidderCatalog; + + public BidderAliases resolve(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest) + .map(PbsUtil::extRequestPrebid) + .map(extRequestPrebid -> { + final Map aliases = extRequestPrebid.getAliases(); + final Map aliasesGvlIds = extRequestPrebid.getAliasgvlids(); + return BidderAliases.of(aliases, aliasesGvlIds, bidderCatalog); + }) + .orElseGet(() -> BidderAliases.of(Map.of(), Map.of(), bidderCatalog)); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index 2a60389802c..81c82b55580 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -1,39 +1,15 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Data; -import com.iab.openrtb.request.Eid; -import com.iab.openrtb.request.Segment; -import com.iab.openrtb.request.Uid; -import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.function.Function; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -public class BidRequestEnricher implements PayloadUpdate { - - private static final String OPTABLE_CO_INSERTER = "optable.co"; - - private final TargetingResult targetingResult; - private final OptableTargetingProperties targetingProperties; +public class BidRequestEnricher extends RequestEnricher implements PayloadUpdate { private BidRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) { - this.targetingResult = targetingResult; - this.targetingProperties = targetingProperties; + super(targetingResult, targetingProperties); } public static BidRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) { @@ -44,160 +20,4 @@ public static BidRequestEnricher of(TargetingResult targetingResult, OptableTarg public AuctionRequestPayload apply(AuctionRequestPayload payload) { return AuctionRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest())); } - - private BidRequest enrichBidRequest(BidRequest bidRequest) { - if (bidRequest == null || targetingResult == null) { - return bidRequest; - } - - final User optableUser = Optional.of(targetingResult) - .map(TargetingResult::getOrtb2) - .map(Ortb2::getUser) - .orElse(null); - - if (optableUser == null) { - return bidRequest; - } - - final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser()) - .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); - - return bidRequest.toBuilder() - .user(mergeUserData(bidRequestUser, optableUser)) - .build(); - } - - private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { - return user.toBuilder() - .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids()))) - .data(mergeData(user.getData(), optableUser.getData())) - .build(); - } - - private List mergeEids(List destination, List source) { - if (CollectionUtils.isEmpty(destination)) { - return source; - } - - if (CollectionUtils.isEmpty(source)) { - return destination; - } - - final Map idToSourceEid = source.stream().collect(Collectors.toMap( - BidRequestEnricher::eidIdExtractor, - Function.identity(), - (a, b) -> b, - HashMap::new)); - - final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace(); - final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge() - .stream() - .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet()); - - final List mergedEid = destination.stream() - .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid)) - && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter()) - ? resolveEidConflict( - destinationEid, - idToSourceEid.get(eidIdExtractor(destinationEid)), - sourceToMerge, - sourceToReplace) - : destinationEid) - .toList(); - - return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor); - } - - private List filterOptableEids(List eids) { - if (CollectionUtils.isEmpty(eids)) { - return eids; - } - - final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore(); - if (CollectionUtils.isEmpty(optableIdsToIgnore)) { - return eids; - } - - return eids.stream() - .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter()) - || !optableIdsToIgnore.contains(eid.getSource())) - .toList(); - } - - private static Eid resolveEidConflict(Eid destinationEid, - Eid sourceEid, - Set sourceToMerge, - Set sourceToReplace) { - - final String eidSource = sourceEid.getSource(); - - if (sourceToReplace.contains(eidSource)) { - return sourceEid; - } - if (sourceToMerge.contains(eidSource)) { - return mergeEid(destinationEid, sourceEid); - } - - return destinationEid; - } - - private static Eid mergeEid(Eid destinationEid, Eid sourceEid) { - return destinationEid.toBuilder() - .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId)) - .build(); - } - - private static String eidIdExtractor(Eid eid) { - return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource()); - } - - private static List mergeData(List destination, List source) { - if (CollectionUtils.isEmpty(destination)) { - return source; - } - - if (CollectionUtils.isEmpty(source)) { - return destination; - } - - final Map idToSourceData = source.stream() - .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new)); - - final List mergedData = destination.stream() - .map(destinationData -> idToSourceData.containsKey(destinationData.getId()) - ? mergeData(destinationData, idToSourceData.get(destinationData.getId())) - : destinationData) - .toList(); - - return merge(mergedData, source, Data::getId); - } - - private static Data mergeData(Data destinationData, Data sourceData) { - return destinationData.toBuilder() - .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId)) - .build(); - } - - private static List merge(List destination, - List source, - Function idExtractor) { - - if (CollectionUtils.isEmpty(source)) { - return destination; - } - - if (CollectionUtils.isEmpty(destination)) { - return source; - } - - final Set existingIds = destination.stream() - .map(idExtractor) - .collect(Collectors.toSet()); - - return Stream.concat( - destination.stream(), - source.stream() - .filter(entry -> !existingIds.contains(idExtractor.apply(entry)))) - .toList(); - } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java new file mode 100644 index 00000000000..ab21c74cc75 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java @@ -0,0 +1,61 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import lombok.AllArgsConstructor; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.util.StreamUtil; + +import java.util.Collection; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.Collectors; + +@AllArgsConstructor(staticName = "of") +public class BidderEnrichmentDicer { + + private final AliasesResolver aliasesResolver; + + public Set dice(BidRequest bidRequest, OptableTargetingProperties optableTargetingProperties) { + final Integer defaultEnrichmentPercentage = optableTargetingProperties.getEnrichmentPercentage(); + final Map bidderEnrichmentPercentage = + optableTargetingProperties.getBidderEnrichmentPercentages(); + + final BidderAliases aliases = aliasesResolver.resolve(bidRequest); + final Set bidders = extractUniqueBidders(bidRequest) + .stream() + .filter(bidder -> { + final int percentage = + resolvePercentage(aliases, bidder, defaultEnrichmentPercentage, bidderEnrichmentPercentage); + return ThreadLocalRandom.current().nextInt(100) <= percentage; + }) + .collect(Collectors.toSet()); + + return bidders; + } + + private static int resolvePercentage(BidderAliases aliases, String bidder, + Integer defaultEnrichmentPercentage, + Map bidderEnrichmentPercentage) { + + return Optional.ofNullable(bidderEnrichmentPercentage.get(bidder)) + .or(() -> Optional.ofNullable(bidderEnrichmentPercentage.get(aliases.resolveBidder(bidder)))) + .orElse(defaultEnrichmentPercentage); + } + + private static Set extractUniqueBidders(BidRequest bidRequest) { + return Optional.ofNullable(bidRequest.getImp()) + .stream() + .flatMap(Collection::stream) + .map(Imp::getExt) + .filter(Objects::nonNull) + .map(ext -> ext.at("/prebid/bidder")) + .filter(Objects::nonNull) + .flatMap(bidder -> StreamUtil.asStream(bidder.fieldNames())) + .collect(Collectors.toSet()); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java new file mode 100644 index 00000000000..24b59cd42a1 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderRequestEnricher.java @@ -0,0 +1,23 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +public class BidderRequestEnricher extends RequestEnricher implements PayloadUpdate { + + private BidderRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) { + super(targetingResult, targetingProperties); + } + + public static BidderRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) { + return new BidderRequestEnricher(targetingResult, properties); + } + + @Override + public BidderRequestPayload apply(BidderRequestPayload payload) { + return BidderRequestPayloadImpl.of(enrichBidRequest(payload.bidRequest())); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java new file mode 100644 index 00000000000..634fcb000cc --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/RequestEnricher.java @@ -0,0 +1,191 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class RequestEnricher { + + private static final String OPTABLE_CO_INSERTER = "optable.co"; + + private final TargetingResult targetingResult; + private final OptableTargetingProperties targetingProperties; + + protected RequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) { + this.targetingResult = targetingResult; + this.targetingProperties = targetingProperties; + } + + protected BidRequest enrichBidRequest(BidRequest bidRequest) { + if (bidRequest == null || targetingResult == null) { + return bidRequest; + } + + final User optableUser = Optional.of(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .orElse(null); + + if (optableUser == null) { + return bidRequest; + } + + final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser()) + .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); + + return bidRequest.toBuilder() + .user(mergeUserData(bidRequestUser, optableUser)) + .build(); + } + + private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { + return user.toBuilder() + .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids()))) + .data(mergeData(user.getData(), optableUser.getData())) + .build(); + } + + private List mergeEids(List destination, List source) { + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map idToSourceEid = source.stream().collect(Collectors.toMap( + RequestEnricher::eidIdExtractor, + Function.identity(), + (a, b) -> b, + HashMap::new)); + + final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace(); + final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge() + .stream() + .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet()); + + final List mergedEid = destination.stream() + .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid)) + && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter()) + ? resolveEidConflict( + destinationEid, + idToSourceEid.get(eidIdExtractor(destinationEid)), + sourceToMerge, + sourceToReplace) + : destinationEid) + .toList(); + + return merge(mergedEid, source, RequestEnricher::eidIdExtractor); + } + + private List filterOptableEids(List eids) { + if (CollectionUtils.isEmpty(eids)) { + return eids; + } + + final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore(); + if (CollectionUtils.isEmpty(optableIdsToIgnore)) { + return eids; + } + + return eids.stream() + .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter()) + || !optableIdsToIgnore.contains(eid.getSource())) + .toList(); + } + + private static Eid resolveEidConflict(Eid destinationEid, + Eid sourceEid, + Set sourceToMerge, + Set sourceToReplace) { + + final String eidSource = sourceEid.getSource(); + + if (sourceToReplace.contains(eidSource)) { + return sourceEid; + } + if (sourceToMerge.contains(eidSource)) { + return mergeEid(destinationEid, sourceEid); + } + + return destinationEid; + } + + private static Eid mergeEid(Eid destinationEid, Eid sourceEid) { + return destinationEid.toBuilder() + .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId)) + .build(); + } + + private static String eidIdExtractor(Eid eid) { + return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource()); + } + + private static List mergeData(List destination, List source) { + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map idToSourceData = source.stream() + .collect(Collectors.toMap(Data::getId, Function.identity(), (a, b) -> b, HashMap::new)); + + final List mergedData = destination.stream() + .map(destinationData -> idToSourceData.containsKey(destinationData.getId()) + ? mergeData(destinationData, idToSourceData.get(destinationData.getId())) + : destinationData) + .toList(); + + return merge(mergedData, source, Data::getId); + } + + private static Data mergeData(Data destinationData, Data sourceData) { + return destinationData.toBuilder() + .segment(merge(destinationData.getSegment(), sourceData.getSegment(), Segment::getId)) + .build(); + } + + private static List merge(List destination, + List source, + Function idExtractor) { + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + final Set existingIds = destination.stream() + .map(idExtractor) + .collect(Collectors.toSet()); + + return Stream.concat( + destination.stream(), + source.stream() + .filter(entry -> !existingIds.contains(idExtractor.apply(entry)))) + .toList(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java index 00a0bbf77ee..5815703db8a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -14,6 +14,7 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentDicer; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; @@ -21,6 +22,8 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import java.util.Set; + import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -42,6 +45,8 @@ public class OptableRawAuctionRequestHookTest extends BaseOptableTest { private AuctionInvocationContext invocationContext; @Mock private Timeout timeout; + @Mock + private BidderEnrichmentDicer bidderEnrichmentDicer; private ConfigResolver configResolver; private NetworkCall networkCall; @@ -53,7 +58,7 @@ public void setUp() { .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, 0.01); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentDicer, 0.01); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); when(invocationContext.timeout()).thenReturn(timeout); when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); @@ -75,6 +80,7 @@ public void shouldInjectEarlyNetworkCallToModuleContext(VertxTestContext vertxTe when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); + when(bidderEnrichmentDicer.dice(any(), any())).thenReturn(Set.of("bidder")); // when final Future> result = @@ -106,7 +112,7 @@ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAc configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true)); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, 0.01); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentDicer, 0.01); // when final Future> result = diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 571ad0a5a97..c1b62985b95 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -21,6 +21,12 @@ accounts: api-key: key tenant: optable origin: web-sdk-demo + enrichment-percentage: 100 + bidder-enrichment-percentages: + appnexus: 75 + rubicon: 75 + pubmatic: 100 + criteo: 0 ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: @@ -44,6 +50,19 @@ accounts: } ] }, + "bidder-request": { + "groups": [ + { + "timeout": 500, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-bidder-request-hook" + } + ] + } + ] + }, "processed-auction-request": { "groups": [ { From 57582cbfdeb9c6317f9d561101fcba22293d1e17 Mon Sep 17 00:00:00 2001 From: softcoder Date: Wed, 13 May 2026 20:43:12 +0200 Subject: [PATCH 03/19] optable-targeting: Add unit tests, rename dicer to sampler --- .../config/OptableTargetingConfig.java | 4 +- .../v1/OptableRawAuctionRequestHook.java | 10 +- ...icer.java => BidderEnrichmentSampler.java} | 16 +- .../v1/OptableBidderRequestHookTest.java | 227 ++++++++++++++ .../v1/OptableRawAuctionRequestHookTest.java | 10 +- .../v1/core/AliasesResolverTest.java | 88 ++++++ .../v1/core/BidderEnrichmentSamplerTest.java | 284 ++++++++++++++++++ 7 files changed, 621 insertions(+), 18 deletions(-) rename extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/{BidderEnrichmentDicer.java => BidderEnrichmentSampler.java} (81%) create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.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 0d10536ffa6..d05bf24d59b 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 @@ -11,7 +11,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AliasesResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentDicer; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; @@ -108,7 +108,7 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, new OptableRawAuctionRequestHook( configResolver, networkCall, - BidderEnrichmentDicer.of(AliasesResolver.of(bidderCatalog)), + BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog)), logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index eaa1561f23c..35d882dde9d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -9,7 +9,7 @@ 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.BidderEnrichmentDicer; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.v1.InvocationAction; @@ -33,17 +33,17 @@ public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { private final ConfigResolver configResolver; private final NetworkCall networkCall; - private final BidderEnrichmentDicer bidderEnrichmentDicer; + private final BidderEnrichmentSampler bidderEnrichmentSampler; private final double logSamplingRate; public OptableRawAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, - BidderEnrichmentDicer bidderEnrichmentDicer, + BidderEnrichmentSampler bidderEnrichmentSampler, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); this.networkCall = Objects.requireNonNull(networkCall); - this.bidderEnrichmentDicer = Objects.requireNonNull(bidderEnrichmentDicer); + this.bidderEnrichmentSampler = Objects.requireNonNull(bidderEnrichmentSampler); this.logSamplingRate = logSamplingRate; } @@ -68,7 +68,7 @@ public Future> call(AuctionRequestPayloa } final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest(); - final Set biddersToEnrich = bidderEnrichmentDicer.dice(bidRequest, properties); + final Set biddersToEnrich = bidderEnrichmentSampler.sample(bidRequest, properties); if (CollectionUtils.isEmpty(biddersToEnrich)) { return OptableHook.update(BidRequestCleaner.instance(), moduleContext); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java similarity index 81% rename from extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java rename to extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java index ab21c74cc75..95fe7820176 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentDicer.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java @@ -13,29 +13,33 @@ import java.util.Optional; import java.util.Set; import java.util.concurrent.ThreadLocalRandom; +import java.util.function.IntSupplier; import java.util.stream.Collectors; @AllArgsConstructor(staticName = "of") -public class BidderEnrichmentDicer { +public class BidderEnrichmentSampler { private final AliasesResolver aliasesResolver; + private final IntSupplier randomSupplier; - public Set dice(BidRequest bidRequest, OptableTargetingProperties optableTargetingProperties) { + public static BidderEnrichmentSampler of(AliasesResolver aliasesResolver) { + return of(aliasesResolver, () -> ThreadLocalRandom.current().nextInt(100)); + } + + public Set sample(BidRequest bidRequest, OptableTargetingProperties optableTargetingProperties) { final Integer defaultEnrichmentPercentage = optableTargetingProperties.getEnrichmentPercentage(); final Map bidderEnrichmentPercentage = optableTargetingProperties.getBidderEnrichmentPercentages(); final BidderAliases aliases = aliasesResolver.resolve(bidRequest); - final Set bidders = extractUniqueBidders(bidRequest) + return extractUniqueBidders(bidRequest) .stream() .filter(bidder -> { final int percentage = resolvePercentage(aliases, bidder, defaultEnrichmentPercentage, bidderEnrichmentPercentage); - return ThreadLocalRandom.current().nextInt(100) <= percentage; + return randomSupplier.getAsInt() <= percentage; }) .collect(Collectors.toSet()); - - return bidders; } private static int resolvePercentage(BidderAliases aliases, String bidder, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java new file mode 100644 index 00000000000..c68e58bb5a6 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -0,0 +1,227 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import com.iab.openrtb.request.BidRequest; +import io.vertx.core.Future; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; +import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; + +import java.util.Collections; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +public class OptableBidderRequestHookTest extends BaseOptableTest { + + @Mock + private BidderInvocationContext invocationContext; + + @Mock + private BidderRequestPayload bidderRequestPayload; + + private OptableBidderRequestHook target; + + @BeforeEach + public void setUp() { + target = new OptableBidderRequestHook(); + when(bidderRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + } + + @Test + public void shouldHaveRightCode() { + // given and when and then + assertThat(target.code()).isEqualTo("optable-targeting-bidder-request-hook"); + } + + @Test + public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenOptableTargetingProperties(false)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result.moduleContext()).isSameAs(moduleContext); + } + + @Test + public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Collections.emptySet()); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void shouldReturnNoActionWhenBiddersToEnrichIsNull() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + + final BidRequest enrichedRequest = result + .payloadUpdate() + .apply(BidderRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(enrichedRequest.getUser().getEids().getFirst().getUids().getFirst().getId()) + .isEqualTo("id"); + assertThat(enrichedRequest.getUser().getData().getFirst().getSegment().getFirst().getId()) + .isEqualTo("id"); + } + + @Test + public void shouldUpdateModuleContextWithTargetingOnSuccess() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + target.call(bidderRequestPayload, invocationContext); + + // then — the moduleContext is mutated in-place with audience targeting + assertThat(moduleContext.getTargeting()).isNotNull().isNotEmpty(); + assertThat(moduleContext.getEnrichRequestStatus()).isNotNull(); + assertThat(moduleContext.getEnrichRequestStatus().getStatus().getValue()).isEqualTo("success"); + } + + @Test + public void shouldReturnNoActionWhenTargetingCallFails() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingCall( + Future.failedFuture(new RuntimeException("targeting service error"))); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.no_action); + } + + @Test + public void shouldIncludeAnalyticsTagsInNoActionResponse() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenOptableTargetingProperties(false)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + final InvocationResult result = future.result(); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags().activities()).isNotEmpty(); + assertThat(result.analyticsTags().activities().getFirst().name()) + .isEqualTo("optable-enrich-request"); + } + + @Test + public void shouldIncludeAnalyticsTagsInUpdateResponse() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + final InvocationResult result = future.result(); + assertThat(result.analyticsTags()).isNotNull(); + assertThat(result.analyticsTags().activities()).isNotEmpty(); + assertThat(result.analyticsTags().activities().getFirst().name()) + .isEqualTo("optable-enrich-request"); + } + + private static ModuleContext givenModuleContextWithProperties(OptableTargetingProperties properties) { + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setOptableTargetingProperties(properties); + return moduleContext; + } + + private OptableTargetingProperties givenPropertiesWithPerBidderEnrichmentEnabled() { + final OptableTargetingProperties properties = givenOptableTargetingProperties(false); + properties.setEnrichmentPercentage(50); + return properties; + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java index 5815703db8a..ad438a53fd1 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -14,7 +14,7 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentDicer; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; @@ -46,7 +46,7 @@ public class OptableRawAuctionRequestHookTest extends BaseOptableTest { @Mock private Timeout timeout; @Mock - private BidderEnrichmentDicer bidderEnrichmentDicer; + private BidderEnrichmentSampler bidderEnrichmentSampler; private ConfigResolver configResolver; private NetworkCall networkCall; @@ -58,7 +58,7 @@ public void setUp() { .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentDicer, 0.01); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); when(invocationContext.timeout()).thenReturn(timeout); when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); @@ -80,7 +80,7 @@ public void shouldInjectEarlyNetworkCallToModuleContext(VertxTestContext vertxTe when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); - when(bidderEnrichmentDicer.dice(any(), any())).thenReturn(Set.of("bidder")); + when(bidderEnrichmentSampler.sample(any(), any())).thenReturn(Set.of("bidder")); // when final Future> result = @@ -112,7 +112,7 @@ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAc configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true)); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentDicer, 0.01); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01); // when final Future> result = diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java new file mode 100644 index 00000000000..b8b7c986636 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/AliasesResolverTest.java @@ -0,0 +1,88 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.bidder.BidderCatalog; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public class AliasesResolverTest { + + @Mock + private BidderCatalog bidderCatalog; + + private AliasesResolver target; + + @BeforeEach + public void setUp() { + target = AliasesResolver.of(bidderCatalog); + } + + @Test + public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestIsNull() { + // when + final BidderAliases result = target.resolve(null); + + // then + assertThat(result.isAliasDefined("anyAlias")).isFalse(); + } + + @Test + public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestHasNoExt() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final BidderAliases result = target.resolve(bidRequest); + + // then + assertThat(result.isAliasDefined("anyAlias")).isFalse(); + } + + @Test + public void resolveShouldReturnEmptyBidderAliasesWhenBidRequestHasNoExtPrebid() { + // given + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.empty()) + .build(); + + // when + final BidderAliases result = target.resolve(bidRequest); + + // then + assertThat(result.isAliasDefined("anyAlias")).isFalse(); + } + + @Test + public void resolveShouldReturnBidderAliasesWithValuesWhenBidRequestHasAliases() { + // given + final BidRequest bidRequest = BidRequest.builder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .aliases(Map.of("alias", "bidder")) + .aliasgvlids(Map.of("alias", 123)) + .build())) + .build(); + + given(bidderCatalog.isValidName(anyString())).willReturn(false); + + // when + final BidderAliases result = target.resolve(bidRequest); + + // then + assertThat(result.isAliasDefined("alias")).isTrue(); + assertThat(result.resolveBidder("alias")).isEqualTo("bidder"); + assertThat(result.resolveAliasVendorId("alias")).isEqualTo(123); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java new file mode 100644 index 00000000000..27220fd9b1d --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java @@ -0,0 +1,284 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Imp; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.prebid.server.auction.aliases.BidderAliases; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.IntSupplier; +import java.util.function.UnaryOperator; + +import static java.util.function.UnaryOperator.identity; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +@ExtendWith(MockitoExtension.class) +public class BidderEnrichmentSamplerTest extends BaseOptableTest { + + @Mock + private AliasesResolver aliasesResolver; + + @Mock + private BidderAliases bidderAliases; + + @Mock + private IntSupplier randomSupplier; + + private BidderEnrichmentSampler target; + + @BeforeEach + public void setUp() { + target = BidderEnrichmentSampler.of(aliasesResolver, randomSupplier); + given(aliasesResolver.resolve(any())).willReturn(bidderAliases); + } + + @Test + public void diceShouldReturnEmptySetWhenRequestHasNoImpressions() { + // given + final BidRequest bidRequest = givenBidRequest(identity()); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldReturnEmptySetWhenImpHasNoExt() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(identity())))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldReturnEmptySetWhenImpExtHasNoPrebidBidderNode() { + // given + final ObjectNode ext = mapper.createObjectNode(); + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(ext))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldReturnEmptySetWhenBidderNodeIsEmpty() { + // given + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt()))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldIncludeAllBiddersWhenDefaultPercentageIs100() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(99); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + + // then + assertThat(result).containsExactlyInAnyOrder("bidderA", "bidderB"); + } + + @Test + public void diceShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(0); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(-1, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldIncludeBidderWhenRandomValueEqualsPercentage() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(50); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(50, Collections.emptyMap())); + + // then + assertThat(result).containsExactly("bidderA"); + } + + @Test + public void diceShouldIncludeBidderWhenRandomValueIsBelowPercentage() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(49); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(50, Collections.emptyMap())); + + // then + assertThat(result).containsExactly("bidderA"); + } + + @Test + public void diceShouldExcludeBidderWhenRandomValueExceedsPercentage() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(51); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(50, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldIncludeBidderWhenPercentageIsZeroAndRandomIsZero() { + // given — 0% still includes exactly when random == 0 + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(0); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(0, Collections.emptyMap())); + + // then + assertThat(result).containsExactly("bidderA"); + } + + @Test + public void diceShouldExcludeBidderWhenPercentageIsZeroAndRandomIsOne() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(1); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(0, Collections.emptyMap())); + + // then + assertThat(result).isEmpty(); + } + + @Test + public void diceShouldUseBidderSpecificPercentageWhenAvailable() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(99); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(-1, Map.of("bidderA", 100))); + + // then + assertThat(result).containsExactly("bidderA"); + } + + @Test + public void diceShouldUseAliasSpecificPercentageWhenBidderResolvesToAlias() { + // given + given(bidderAliases.resolveBidder("bidderA")).willReturn("bidderA"); + given(bidderAliases.resolveBidder("bidderB")).willReturn("aliasB"); + given(randomSupplier.getAsInt()).willReturn(99); + + final BidRequest bidRequest = givenBidRequest( + request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(-1, Map.of("aliasB", 100))); + + // then + assertThat(result).containsExactly("bidderB"); + } + + @Test + public void diceShouldDeduplicateBiddersAppearingInMultipleImps() { + // given + given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); + given(randomSupplier.getAsInt()).willReturn(0); + + final ObjectNode ext = givenBidderExt("bidderA"); + final BidRequest bidRequest = givenBidRequest(request -> request.imp(List.of( + givenImp(imp -> imp.ext(ext)), + givenImp(imp -> imp.ext(ext))))); + + // when + final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + + // then + assertThat(result).containsExactly("bidderA"); + } + + private OptableTargetingProperties givenDiceProperties(int defaultPct, Map bidderPcts) { + final OptableTargetingProperties props = new OptableTargetingProperties(); + props.setEnrichmentPercentage(defaultPct); + props.setBidderEnrichmentPercentages(bidderPcts); + return props; + } + + private ObjectNode givenBidderExt(String... bidders) { + final ObjectNode bidderNode = mapper.createObjectNode(); + for (String bidder : bidders) { + bidderNode.put(bidder, "value"); + } + final ObjectNode prebidNode = mapper.createObjectNode(); + prebidNode.set("bidder", bidderNode); + final ObjectNode ext = mapper.createObjectNode(); + ext.set("prebid", prebidNode); + return ext; + } + + private static Imp givenImp(UnaryOperator impCustomizer) { + return impCustomizer.apply(Imp.builder()).build(); + } +} From cd70c5d2ebc32efea6202a09764d0f262aef0531 Mon Sep 17 00:00:00 2001 From: softcoder Date: Wed, 13 May 2026 22:11:17 +0200 Subject: [PATCH 04/19] optable-targeting: add Web vs. Mobile Traffic Toggle --- .../targeting/model/ModuleContext.java | 2 + .../config/OptableTargetingProperties.java | 4 + .../optable/targeting/v1/OptableHook.java | 36 ----- .../v1/OptableRawAuctionRequestHook.java | 27 +++- .../OptableTargetingAuctionResponseHook.java | 2 +- ...eTargetingProcessedAuctionRequestHook.java | 8 +- .../v1/core/PropertiesValidator.java | 20 +++ .../optable/targeting/v1/BaseOptableTest.java | 7 +- .../v1/OptableRawAuctionRequestHookTest.java | 36 +++++ ...tableTargetingAuctionResponseHookTest.java | 21 +++ ...getingProcessedAuctionRequestHookTest.java | 22 +++ .../v1/core/PropertiesValidatorTest.java | 126 ++++++++++++++++++ .../configs/sample-app-settings-optable.yaml | 2 + 13 files changed, 271 insertions(+), 42 deletions(-) delete mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java 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 c88b88f5e1b..5574a7a72d8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -33,6 +33,8 @@ public class ModuleContext { private OptableTargetingProperties optableTargetingProperties; + private boolean shouldSkipEnrichment; + public static ModuleContext of(AuctionInvocationContext invocationContext) { 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 0fe5eff4b5b..4db54642903 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java @@ -49,6 +49,10 @@ public final class OptableTargetingProperties { @JsonProperty("bidder-enrichment-percentages") Map bidderEnrichmentPercentages = Map.of(); + Boolean enrichWeb = true; + + Boolean enrichApp = true; + public boolean isPerBidderEnrichmentEnabled() { return enrichmentPercentage != 100 || MapUtils.isNotEmpty(bidderEnrichmentPercentages); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java deleted file mode 100644 index fa22ad5c3df..00000000000 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java +++ /dev/null @@ -1,36 +0,0 @@ -package org.prebid.server.hooks.modules.optable.targeting.v1; - -import io.vertx.core.Future; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.hooks.execution.v1.InvocationResultImpl; -import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; -import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; -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; - -public class OptableHook { - - private OptableHook() { - } - - public static boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { - return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); - } - - public static Future> update( - PayloadUpdate payloadUpdate, - ModuleContext moduleContext) { - - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.update) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) - .payloadUpdate(payloadUpdate) - .moduleContext(moduleContext) - .build()); - } -} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index 35d882dde9d..bd31576ac34 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -12,9 +12,11 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; @@ -57,20 +59,25 @@ public Future> call(AuctionRequestPayloa moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); moduleContext.setOptableTargetingProperties(properties); - if (!OptableHook.isTargetingPropertiesValid(properties)) { + if (!PropertiesValidator.isValid(properties)) { conditionalLogger.error( "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); moduleContext.failWithExecutionTime( System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); - return OptableHook.update(BidRequestCleaner.instance(), moduleContext); + return update(BidRequestCleaner.instance(), moduleContext); } final BidRequest bidRequest = invocationContext.auctionContext().getBidRequest(); + if (!PropertiesValidator.isTrafficSourceValid(bidRequest, properties)) { + moduleContext.setShouldSkipEnrichment(true); + return update(BidRequestCleaner.instance(), moduleContext); + } + final Set biddersToEnrich = bidderEnrichmentSampler.sample(bidRequest, properties); if (CollectionUtils.isEmpty(biddersToEnrich)) { - return OptableHook.update(BidRequestCleaner.instance(), moduleContext); + return update(BidRequestCleaner.instance(), moduleContext); } moduleContext.setBiddersToEnrich(biddersToEnrich); @@ -95,6 +102,20 @@ private static Future> updateModuleConte .build()); } + public static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } + @Override public String code() { return CODE; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 5a20f79a347..8edef6b00eb 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -52,7 +52,7 @@ public Future> call(AuctionResponsePayl final ModuleContext moduleContext = ModuleContext.of(invocationContext); moduleContext.setAdserverTargetingEnabled(adserverTargeting); - if (!adserverTargeting) { + if (moduleContext.isShouldSkipEnrichment() || !adserverTargeting) { return success(moduleContext); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index c506ad03b90..e82ac21fc3d 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 @@ -11,6 +11,7 @@ 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.NetworkCall; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -51,6 +52,11 @@ public Future> call(AuctionRequestPayloa AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = ModuleContext.of(invocationContext); + if (moduleContext.isShouldSkipEnrichment()) { + moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); + return update(BidRequestCleaner.instance(), moduleContext); + } + final OptableTargetingProperties properties = resolveOptableTargetingProperties(moduleContext, invocationContext); @@ -117,7 +123,7 @@ private Future resolvePreEarlyNetworkCall( OptableTargetingProperties properties) { moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); - if (!OptableHook.isTargetingPropertiesValid(properties)) { + if (!PropertiesValidator.isValid(properties)) { conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate); moduleContext.failWithExecutionTime( diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java new file mode 100644 index 00000000000..391590057ff --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidator.java @@ -0,0 +1,20 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; + +public class PropertiesValidator { + + private PropertiesValidator() { + } + + public static boolean isValid(OptableTargetingProperties properties) { + return StringUtils.isNotEmpty(properties.getOrigin()) && StringUtils.isNotEmpty(properties.getTenant()); + } + + public static boolean isTrafficSourceValid(BidRequest bidRequest, OptableTargetingProperties properties) { + return (Boolean.TRUE.equals(properties.getEnrichWeb()) && bidRequest.getSite() != null) + || (Boolean.TRUE.equals(properties.getEnrichApp()) && bidRequest.getApp() != null); + } +} diff --git a/extra/modules/optable-targeting/src/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 90f2008cec7..194cd3282a7 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -10,6 +10,7 @@ import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Geo; import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Site; import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; import com.iab.openrtb.response.Bid; @@ -92,13 +93,14 @@ protected BidRequest givenBidRequest() { } protected static BidRequest givenBidRequest(UnaryOperator bidRequestCustomizer) { - return bidRequestCustomizer.apply(BidRequest.builder().id("requestId")).build(); + return bidRequestCustomizer.apply(BidRequest.builder().id("requestId").site(Site.builder().build())).build(); } protected BidRequest givenBidRequestWithUserEids(List eids) { return BidRequest.builder() .user(givenUser(eids)) .device(givenDevice()) + .site(Site.builder().build()) .cur(List.of("USD")) .build(); } @@ -107,6 +109,7 @@ protected BidRequest givenBidRequestWithUser(User user) { return BidRequest.builder() .user(user) .device(givenDevice()) + .site(Site.builder().build()) .cur(List.of("USD")) .build(); } @@ -115,6 +118,7 @@ protected BidRequest givenBidRequestWithUserData(List data) { return BidRequest.builder() .user(givenUserWithData(data)) .device(givenDevice()) + .site(Site.builder().build()) .cur(List.of("USD")) .build(); } @@ -253,6 +257,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, optableTargetingProperties.setApiKey(key); optableTargetingProperties.setPpidMapping(Map.of("c", "id")); optableTargetingProperties.setAdserverTargeting(true); + optableTargetingProperties.setEnrichWeb(true); optableTargetingProperties.setTimeout(100L); optableTargetingProperties.setCache(cacheProperties); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java index ad438a53fd1..f5e6a39663e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; @@ -130,4 +131,39 @@ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAc vertxTestContext.completeNow(); }); } + + @SneakyThrows + @Test + public void shouldNotInjectEarlyNetworkCallWhenTrafficSourceIsInvalid(VertxTestContext vertxTestContext) { + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", "origin", true)); + final BidRequest bidRequestWithoutTrafficSource = givenBidRequest(bidRequestCustomizer -> + bidRequestCustomizer.site(null).app(null)); + when(auctionRequestPayload.bidRequest()).thenReturn(bidRequestWithoutTrafficSource); + when(invocationContext.auctionContext()).thenReturn( + givenAuctionContext(activityInfrastructure, timeout) + .toBuilder() + .bidRequest(bidRequestWithoutTrafficSource) + .build()); + + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01); + + // when + final Future> result = + target.call(auctionRequestPayload, invocationContext); + + // then + assertThat(result).isNotNull(); + result.map(res -> (ModuleContext) res.moduleContext()) + .onComplete(cxt -> { + vertxTestContext.verify(() -> { + final ModuleContext moduleContext = cxt.result(); + assertThat(moduleContext.isShouldSkipEnrichment()).isTrue(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + }); + vertxTestContext.completeNow(); + }); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index af4a809df78..cf67cfdad0a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -9,6 +9,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -139,6 +140,26 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { .returns(InvocationAction.no_action, InvocationResult::action); } + @Test + public void shouldReturnSuccessWhenSkipEnrichmentIsTrue() { + // given + final ModuleContext moduleContext = givenModuleContext(); + moduleContext.setShouldSkipEnrichment(true); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + } + private ObjectNode givenAccountConfig(boolean cacheEnabled) { return mapper.valueToTree(givenOptableTargetingProperties(cacheEnabled)); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 190004eb758..0422c102d76 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 @@ -272,6 +272,28 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() assertThat(result.errors()).isNull(); } + @Test + public void shouldReturnUpdateWhenTrafficSourceIsInvalid() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setShouldSkipEnrichment(true); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + } + private ObjectNode givenAccountConfig(boolean cacheEnabled) { return givenAccountConfig("key", "tenant", "origin", cacheEnabled); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java new file mode 100644 index 00000000000..0f496051b65 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/PropertiesValidatorTest.java @@ -0,0 +1,126 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.App; +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Site; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; + +import static org.assertj.core.api.Assertions.assertThat; + +public class PropertiesValidatorTest { + + @Test + public void isValidShouldReturnTrueWhenTenantAndOriginArePresent() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setTenant("tenant"); + properties.setOrigin("origin"); + + // when + final boolean result = PropertiesValidator.isValid(properties); + + // then + assertThat(result).isTrue(); + } + + @Test + public void isValidShouldReturnFalseWhenTenantIsMissing() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOrigin("origin"); + + // when + final boolean result = PropertiesValidator.isValid(properties); + + // then + assertThat(result).isFalse(); + } + + @Test + public void isValidShouldReturnFalseWhenOriginIsMissing() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setTenant("tenant"); + + // when + final boolean result = PropertiesValidator.isValid(properties); + + // then + assertThat(result).isFalse(); + } + + @Test + public void isTrafficSourceValidShouldReturnTrueWhenEnrichWebIsTrueAndSiteIsPresent() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setEnrichWeb(true); + final BidRequest bidRequest = BidRequest.builder().site(Site.builder().build()).build(); + + // when + final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties); + + // then + assertThat(result).isTrue(); + } + + @Test + public void isTrafficSourceValidShouldReturnFalseWhenEnrichWebIsTrueAndSiteIsMissing() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setEnrichWeb(true); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties); + + // then + assertThat(result).isFalse(); + } + + @Test + public void isTrafficSourceValidShouldReturnTrueWhenEnrichAppIsTrueAndAppIsPresent() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setEnrichApp(true); + final BidRequest bidRequest = BidRequest.builder().app(App.builder().build()).build(); + + // when + final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties); + + // then + assertThat(result).isTrue(); + } + + @Test + public void isTrafficSourceValidShouldReturnFalseWhenEnrichAppIsTrueAndAppIsMissing() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setEnrichApp(true); + final BidRequest bidRequest = BidRequest.builder().build(); + + // when + final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties); + + // then + assertThat(result).isFalse(); + } + + @Test + public void isTrafficSourceValidShouldReturnFalseWhenBothEnrichWebAndEnrichAppAreFalseOrNull() { + // given + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setEnrichWeb(false); + properties.setEnrichApp(false); + final BidRequest bidRequest = BidRequest.builder() + .site(Site.builder().build()) + .app(App.builder().build()) + .build(); + + // when + final boolean result = PropertiesValidator.isTrafficSourceValid(bidRequest, properties); + + // then + assertThat(result).isFalse(); + } +} diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index c1b62985b95..5bb0d589483 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -27,6 +27,8 @@ accounts: rubicon: 75 pubmatic: 100 criteo: 0 + enrich-web: true + enrich-app: true ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: From 477627780ab74d6c89e6b9e8ac14fe45bbd3e8f8 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 14 May 2026 00:25:53 +0200 Subject: [PATCH 05/19] optable-targeting: add hook execution plan helper inside hook --- .../config/OptableTargetingConfig.java | 8 + ...eTargetingProcessedAuctionRequestHook.java | 17 +- .../v1/core/CompositeHookExecutionPlan.java | 50 +++++ ...getingProcessedAuctionRequestHookTest.java | 200 +++++++++++++++--- .../core/CompositeHookExecutionPlanTest.java | 88 ++++++++ 5 files changed, 334 insertions(+), 29 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.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 d05bf24d59b..d34cbcf6321 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,9 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.config; import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.hooks.execution.model.ExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook; @@ -102,6 +104,9 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, NetworkCall networkCall, JsonMerger jsonMerger, BidderCatalog bidderCatalog, + JacksonMapper mapper, + @Value("${hooks.host-execution-plan}") + String executionPlan, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { return new OptableTargetingModule(List.of( @@ -113,6 +118,9 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, new OptableTargetingProcessedAuctionRequestHook( configResolver, networkCall, + StringUtils.isNoneEmpty(executionPlan) + ? mapper.decodeValue(executionPlan, ExecutionPlan.class) + : null, logSamplingRate), new OptableBidderRequestHook(), new OptableTargetingAuctionResponseHook( 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 e82ac21fc3d..2f19ef6bedb 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,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import io.vertx.core.Future; +import org.prebid.server.hooks.execution.model.ExecutionPlan; 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; @@ -9,6 +10,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; @@ -37,13 +39,16 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final ConfigResolver configResolver; private final NetworkCall networkCall; private final double logSamplingRate; + private final ExecutionPlan globalHooksExecutionPlan; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, + ExecutionPlan globalHooksExecutionPlan, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); this.networkCall = Objects.requireNonNull(networkCall); + this.globalHooksExecutionPlan = globalHooksExecutionPlan; this.logSamplingRate = logSamplingRate; } @@ -51,6 +56,14 @@ public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver public Future> call(AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { + final ExecutionPlan accountSpecificHoksExecutionPlan = + java.util.Optional.ofNullable(invocationContext.auctionContext().getAccount()) + .map(org.prebid.server.settings.model.Account::getHooks) + .map(org.prebid.server.settings.model.AccountHooksConfiguration::getExecutionPlan) + .orElse(null); + final CompositeHookExecutionPlan hooksExecutionPlan = + CompositeHookExecutionPlan.of(globalHooksExecutionPlan, accountSpecificHoksExecutionPlan); + final ModuleContext moduleContext = ModuleContext.of(invocationContext); if (moduleContext.isShouldSkipEnrichment()) { moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); @@ -60,7 +73,7 @@ public Future> call(AuctionRequestPayloa final OptableTargetingProperties properties = resolveOptableTargetingProperties(moduleContext, invocationContext); - final Future optableTargetingCall = moduleContext.isEarlyNetworkCallEnabled() + final Future optableTargetingCall = hooksExecutionPlan.hasRawAuctionRequestHook() ? resolveEarlyNetworkCall(moduleContext) : resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties); @@ -73,7 +86,7 @@ public Future> call(AuctionRequestPayloa .compose(targetingResult -> { moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); return enrichPayload( - properties.isPerBidderEnrichmentEnabled(), targetingResult, moduleContext, properties); + hooksExecutionPlan.hasBidderRequestHook(), targetingResult, moduleContext, properties); }) .recover(throwable -> { moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext)); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java new file mode 100644 index 00000000000..243c642fcc8 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java @@ -0,0 +1,50 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import lombok.AllArgsConstructor; +import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionPlan; +import org.prebid.server.model.Endpoint; + +import java.util.List; +import java.util.Optional; + +@AllArgsConstructor(staticName = "of") +public class CompositeHookExecutionPlan { + + private static final String ENDPOINT_AUCTION = "openrtb2_auction"; + private static final String STAGE_RAW_AUCTION_REQUEST = "raw_auction_request"; + private static final String STAGE_BIDDER_REQUEST = "bidder_request"; + private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = "optable-targeting-raw-auction-request-hook"; + private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = "optable-targeting-bidder-request-hook"; + + private ExecutionPlan globalExecutionPlan; + + private ExecutionPlan accountExecutionPlan; + + public boolean hasRawAuctionRequestHook() { + return hasHook(accountExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) + || hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION); + } + + public boolean hasBidderRequestHook() { + return hasHook(accountExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST) + || hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST); + } + + private static boolean hasHook(ExecutionPlan executionPlan, String stage, String hookCode) { + return Optional.ofNullable(executionPlan) + .map(ExecutionPlan::getEndpoints) + .map(endpoints -> endpoints.get(Endpoint.valueOf(ENDPOINT_AUCTION))) + .map(EndpointExecutionPlan::getStages) + .map(stages -> stages.get(Stage.valueOf(stage))) + .map(StageExecutionPlan::getGroups) + .orElseGet(List::of) + .stream() + .map(ExecutionGroup::getHookSequence) + .flatMap(java.util.Collection::stream) + .anyMatch(hook -> hookCode.equals(hook.getHookImplCode())); + } +} diff --git a/extra/modules/optable-targeting/src/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 0422c102d76..6564154179f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -2,17 +2,25 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Data; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.mockito.junit.jupiter.MockitoSettings; -import org.mockito.quality.Strictness; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Status; @@ -24,6 +32,10 @@ import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.model.Endpoint; + +import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -31,27 +43,26 @@ import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) -@MockitoSettings(strictness = Strictness.LENIENT) -public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { +class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { private ConfigResolver configResolver; @Mock private OptableTargeting optableTargeting; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private UserFpdActivityMask userFpdActivityMask; @Mock private AuctionRequestPayload auctionRequestPayload; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private AuctionInvocationContext invocationContext; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private ActivityInfrastructure activityInfrastructure; - @Mock + @Mock(strictness = Mock.Strictness.LENIENT) private Timeout timeout; private NetworkCall networkCall; @@ -59,12 +70,13 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable private OptableTargetingProcessedAuctionRequestHook target; @BeforeEach - public void setUp() { + void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); - target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, ExecutionPlan.empty(), 0.01); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); @@ -74,13 +86,13 @@ public void setUp() { } @Test - public void shouldHaveRightCode() { + void codeShouldReturnRightCode() { // when and then assertThat(target.code()).isEqualTo("optable-targeting-processed-auction-request-hook"); } @Test - public void shouldReturnResultWithPBSAnalyticsTags() { + void callShouldReturnResultWithPBSAnalyticsTags() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) @@ -104,7 +116,7 @@ public void shouldReturnResultWithPBSAnalyticsTags() { } @Test - public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargeting() { + void callShouldReturnResultWithUpdateActionWhenOptableTargetingReturnsTargeting() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) @@ -127,15 +139,22 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) .bidRequest(); - assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); - assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + assertThat(bidRequest.getUser().getEids()) + .flatExtracting(Eid::getUids) + .extracting(Uid::getId) + .containsExactly("id"); + assertThat(bidRequest.getUser().getData()) + .flatExtracting(Data::getSegment) + .extracting(Segment::getId) + .containsExactly("id"); } @Test - public void shouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { + void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { // given final ModuleContext moduleContext = new ModuleContext(); - moduleContext.setEarlyNetworkCallEnabled(true); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, givenExecutionPlan(true, false), 0.01); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); @@ -161,18 +180,126 @@ public void shouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) .bidRequest(); - assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); - assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + assertThat(bidRequest.getUser().getEids()) + .flatExtracting(Eid::getUids) + .extracting(Uid::getId) + .containsExactly("id"); + assertThat(bidRequest.getUser().getData()) + .flatExtracting(Data::getSegment) + .extracting(Segment::getId) + .containsExactly("id"); + } + + @Test + void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { + // given + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, givenExecutionPlan(false, false), 0.01); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + 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()) + .flatExtracting(Eid::getUids) + .extracting(Uid::getId) + .containsExactly("id"); + assertThat(bidRequest.getUser().getData()) + .flatExtracting(Data::getSegment) + .extracting(Segment::getId) + .containsExactly("id"); } @Test - public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { + void callShouldReturnResultWithoutEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() { + // given + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, givenExecutionPlan(false, true), 0.01); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + 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()).isNull(); + assertThat(bidRequest.getUser().getData()).isNull(); + } + + @Test + void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { + // given + final ModuleContext moduleContext = new ModuleContext(); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, givenExecutionPlan(true, true), 0.01); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + moduleContext.setOptableTargetingCall( + networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( + "key", "tenant", "origin", false))); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + 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()).isNull(); + assertThat(bidRequest.getUser().getData()).isNull(); + } + + @Test + void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { // given configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); - target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, ExecutionPlan.empty(), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -194,13 +321,14 @@ public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { } @Test - public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { + void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { // given configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", null, "origin", false)); - target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); + target = new OptableTargetingProcessedAuctionRequestHook( + configResolver, networkCall, ExecutionPlan.empty(), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); @@ -222,9 +350,8 @@ public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { } @Test - public void shouldReturnResultWithCleanedUpUserExtOptableTag() { + void callShouldReturnResultWithCleanedUpUserExtOptableTag() { // given - when(invocationContext.timeout()).thenReturn(timeout); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); @@ -252,7 +379,7 @@ public void shouldReturnResultWithCleanedUpUserExtOptableTag() { } @Test - public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() { + void callShouldReturnResultWithUpdateWhenOptableTargetingDoesNotReturnResult() { // given when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())).thenReturn(Future.succeededFuture(null)); @@ -273,7 +400,7 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() } @Test - public void shouldReturnUpdateWhenTrafficSourceIsInvalid() { + void callShouldReturnUpdateWhenTrafficSourceIsInvalid() { // given final ModuleContext moduleContext = new ModuleContext(); moduleContext.setShouldSkipEnrichment(true); @@ -297,4 +424,23 @@ public void shouldReturnUpdateWhenTrafficSourceIsInvalid() { private ObjectNode givenAccountConfig(boolean cacheEnabled) { return givenAccountConfig("key", "tenant", "origin", cacheEnabled); } + + private ExecutionPlan givenExecutionPlan(boolean hasRawAuctionRequestHook, boolean hasBidderRequestHook) { + final HookId rawAuctionHook = HookId.of("optable-targeting", "optable-targeting-raw-auction-request-hook"); + final HookId bidderRequestHook = HookId.of("optable-targeting", "optable-targeting-bidder-request-hook"); + + final StageExecutionPlan rawAuctionStage = StageExecutionPlan.of(List.of( + ExecutionGroup.of(null, hasRawAuctionRequestHook ? List.of(rawAuctionHook) : List.of()) + )); + final StageExecutionPlan bidderRequestStage = StageExecutionPlan.of(List.of( + ExecutionGroup.of(null, hasBidderRequestHook ? List.of(bidderRequestHook) : List.of()) + )); + + final EndpointExecutionPlan endpointExecutionPlan = EndpointExecutionPlan.of(Map.of( + Stage.raw_auction_request, rawAuctionStage, + Stage.bidder_request, bidderRequestStage + )); + + return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointExecutionPlan)); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java new file mode 100644 index 00000000000..3e262b0c91f --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java @@ -0,0 +1,88 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; +import org.prebid.server.hooks.execution.model.ExecutionGroup; +import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.HookId; +import org.prebid.server.hooks.execution.model.Stage; +import org.prebid.server.hooks.execution.model.StageExecutionPlan; +import org.prebid.server.model.Endpoint; + +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +public class CompositeHookExecutionPlanTest { + + @Test + public void hasRawAuctionRequestHookShouldReturnTrueWhenAccountPlanHasHook() { + // given + final ExecutionPlan accountPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, accountPlan); + + // when and then + assertThat(target.hasRawAuctionRequestHook()).isTrue(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnTrueWhenGlobalPlanHasHook() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan, null); + + // when and then + assertThat(target.hasRawAuctionRequestHook()).isTrue(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnFalseWhenNeitherPlanHasHook() { + // given + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, null); + + // when and then + assertThat(target.hasRawAuctionRequestHook()).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnTrueWhenAccountPlanHasHook() { + // given + final ExecutionPlan accountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, accountPlan); + + // when and then + assertThat(target.hasBidderRequestHook()).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldReturnTrueWhenGlobalPlanHasHook() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan, null); + + // when and then + assertThat(target.hasBidderRequestHook()).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenNeitherPlanHasHook() { + // given + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, null); + + // when and then + assertThat(target.hasBidderRequestHook()).isFalse(); + } + + private ExecutionPlan givenExecutionPlan(String stage, String hookCode) { + final HookId hookId = HookId.of("optable-targeting", hookCode); + final ExecutionGroup group = ExecutionGroup.of(null, List.of(hookId)); + final StageExecutionPlan stagePlan = StageExecutionPlan.of(List.of(group)); + final EndpointExecutionPlan endpointPlan = EndpointExecutionPlan.of(Map.of(Stage.valueOf(stage), stagePlan)); + return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointPlan)); + } +} From d633254f78beff833ffc7236682fd2f019c0f983 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 14 May 2026 19:35:30 +0200 Subject: [PATCH 06/19] optable-targeting: optimize hook execution plan helper. Add results caching --- .../config/OptableTargetingConfig.java | 10 +- ...eTargetingProcessedAuctionRequestHook.java | 26 +-- .../v1/core/CompositeHookExecutionPlan.java | 60 +++++- .../optable/targeting/v1/BaseOptableTest.java | 10 +- ...getingProcessedAuctionRequestHookTest.java | 19 +- .../core/CompositeHookExecutionPlanTest.java | 192 ++++++++++++++++-- 6 files changed, 266 insertions(+), 51 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 d34cbcf6321..1870e8f41f5 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 @@ -15,6 +15,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.AliasesResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; @@ -109,6 +110,11 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, String executionPlan, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { + final CompositeHookExecutionPlan hooksExecutionPlan = CompositeHookExecutionPlan.of( + StringUtils.isNoneEmpty(executionPlan) + ? mapper.decodeValue(executionPlan, ExecutionPlan.class) + : null); + return new OptableTargetingModule(List.of( new OptableRawAuctionRequestHook( configResolver, @@ -118,9 +124,7 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, new OptableTargetingProcessedAuctionRequestHook( configResolver, networkCall, - StringUtils.isNoneEmpty(executionPlan) - ? mapper.decodeValue(executionPlan, ExecutionPlan.class) - : null, + hooksExecutionPlan, logSamplingRate), new OptableBidderRequestHook(), new OptableTargetingAuctionResponseHook( 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 2f19ef6bedb..5d463d67fca 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,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import io.vertx.core.Future; -import org.prebid.server.hooks.execution.model.ExecutionPlan; 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; @@ -23,6 +22,7 @@ import org.prebid.server.hooks.v1.auction.ProcessedAuctionRequestHook; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.LoggerFactory; +import org.prebid.server.settings.model.Account; import java.util.Objects; @@ -39,16 +39,17 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final ConfigResolver configResolver; private final NetworkCall networkCall; private final double logSamplingRate; - private final ExecutionPlan globalHooksExecutionPlan; + + final CompositeHookExecutionPlan hooksExecutionPlan; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, - ExecutionPlan globalHooksExecutionPlan, + CompositeHookExecutionPlan hooksExecutionPlan, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); this.networkCall = Objects.requireNonNull(networkCall); - this.globalHooksExecutionPlan = globalHooksExecutionPlan; + this.hooksExecutionPlan = hooksExecutionPlan; this.logSamplingRate = logSamplingRate; } @@ -56,24 +57,20 @@ public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver public Future> call(AuctionRequestPayload auctionRequestPayload, AuctionInvocationContext invocationContext) { - final ExecutionPlan accountSpecificHoksExecutionPlan = - java.util.Optional.ofNullable(invocationContext.auctionContext().getAccount()) - .map(org.prebid.server.settings.model.Account::getHooks) - .map(org.prebid.server.settings.model.AccountHooksConfiguration::getExecutionPlan) - .orElse(null); - final CompositeHookExecutionPlan hooksExecutionPlan = - CompositeHookExecutionPlan.of(globalHooksExecutionPlan, accountSpecificHoksExecutionPlan); - final ModuleContext moduleContext = ModuleContext.of(invocationContext); if (moduleContext.isShouldSkipEnrichment()) { moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); return update(BidRequestCleaner.instance(), moduleContext); } + final Account account = invocationContext.auctionContext().getAccount(); + final boolean hasRawAuctionRequestHook = hooksExecutionPlan.hasRawAuctionRequestHook(account); + final boolean hasBidderRequestHook = hooksExecutionPlan.hasBidderRequestHook(account); + final OptableTargetingProperties properties = resolveOptableTargetingProperties(moduleContext, invocationContext); - final Future optableTargetingCall = hooksExecutionPlan.hasRawAuctionRequestHook() + final Future optableTargetingCall = hasRawAuctionRequestHook ? resolveEarlyNetworkCall(moduleContext) : resolvePreEarlyNetworkCall(auctionRequestPayload, invocationContext, moduleContext, properties); @@ -85,8 +82,7 @@ public Future> call(AuctionRequestPayloa return optableTargetingCall .compose(targetingResult -> { moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); - return enrichPayload( - hooksExecutionPlan.hasBidderRequestHook(), targetingResult, moduleContext, properties); + return enrichPayload(hasBidderRequestHook, targetingResult, moduleContext, properties); }) .recover(throwable -> { moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext)); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java index 243c642fcc8..814321cc6be 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java @@ -1,17 +1,18 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import lombok.AllArgsConstructor; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.model.Endpoint; +import org.prebid.server.settings.model.Account; import java.util.List; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; -@AllArgsConstructor(staticName = "of") public class CompositeHookExecutionPlan { private static final String ENDPOINT_AUCTION = "openrtb2_auction"; @@ -20,18 +21,57 @@ public class CompositeHookExecutionPlan { private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = "optable-targeting-raw-auction-request-hook"; private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = "optable-targeting-bidder-request-hook"; - private ExecutionPlan globalExecutionPlan; + private final boolean hasGlobalRawAuctionRequestHook; - private ExecutionPlan accountExecutionPlan; + private final boolean hasGlobalBidderRequestHook; - public boolean hasRawAuctionRequestHook() { - return hasHook(accountExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) - || hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION); + private final ConcurrentHashMap rawAuctionRequestHookCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>(); + + private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook, boolean hasGlobalBidderRequestHook) { + this.hasGlobalRawAuctionRequestHook = hasGlobalRawAuctionRequestHook; + this.hasGlobalBidderRequestHook = hasGlobalBidderRequestHook; + } + + public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) { + return globalExecutionPlan == null + ? new CompositeHookExecutionPlan(false, false) + : new CompositeHookExecutionPlan( + hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION), + hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)); + } + + public boolean hasRawAuctionRequestHook(Account account) { + final String accountId = account != null ? account.getId() : null; + + return StringUtils.isNotEmpty(accountId) + ? rawAuctionRequestHookCache.computeIfAbsent(accountId, id -> { + final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account); + return hasHook( + accountSpecificHoksExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) + || hasGlobalRawAuctionRequestHook; + }) + : false; + } + + public boolean hasBidderRequestHook(Account account) { + final String accountId = account != null ? account.getId() : null; + + return StringUtils.isNotEmpty(accountId) + ? bidderRequestHookCache.computeIfAbsent(accountId, id -> { + final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account); + return hasHook( + accountSpecificHoksExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST) + || hasGlobalBidderRequestHook; + }) + : false; } - public boolean hasBidderRequestHook() { - return hasHook(accountExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST) - || hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST); + private ExecutionPlan resolveExecutionPlan(Account account) { + return Optional.ofNullable(account) + .map(org.prebid.server.settings.model.Account::getHooks) + .map(org.prebid.server.settings.model.AccountHooksConfiguration::getExecutionPlan) + .orElse(null); } private static boolean hasHook(ExecutionPlan executionPlan, String stage, String hookCode) { 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 194cd3282a7..159927e8f1d 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -41,6 +41,7 @@ import org.prebid.server.privacy.model.Privacy; import org.prebid.server.privacy.model.PrivacyContext; import org.prebid.server.proto.openrtb.ext.request.ExtUser; +import org.prebid.server.settings.model.Account; import org.prebid.server.vertx.httpclient.model.HttpClientResponse; import java.io.IOException; @@ -72,7 +73,9 @@ protected ModuleContext givenModuleContext(List audiences) { return moduleContext; } - protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) { + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, + Timeout timeout, + Account account) { final GppModel gppModel = new GppModel(); final TcfContext tcfContext = TcfContext.builder().build(); final GppContext gppContext = new GppContext( @@ -81,6 +84,7 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr return AuctionContext.builder() .bidRequest(givenBidRequest()) + .account(account) .activityInfrastructure(activityInfrastructure) .privacyContext(PrivacyContext.of(Privacy.builder().build(), tcfContext, "8.8.8.8")) .gppContext(gppContext) @@ -88,6 +92,10 @@ protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfr .build(); } + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout) { + return givenAuctionContext(activityInfrastructure, timeout, null); + } + protected BidRequest givenBidRequest() { return givenBidRequestWithUserEids(null); } 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 6564154179f..c1082422150 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 @@ -24,6 +24,7 @@ import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; @@ -33,6 +34,7 @@ import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; import org.prebid.server.model.Endpoint; +import org.prebid.server.settings.model.Account; import java.util.List; import java.util.Map; @@ -76,10 +78,11 @@ void setUp() { configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, ExecutionPlan.empty(), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); - when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); + when(invocationContext.auctionContext()).thenReturn( + givenAuctionContext(activityInfrastructure, timeout, Account.builder().id("accountId").build())); when(invocationContext.timeout()).thenReturn(timeout); when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); when(timeout.remaining()).thenReturn(1000L); @@ -154,7 +157,7 @@ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { // given final ModuleContext moduleContext = new ModuleContext(); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, givenExecutionPlan(true, false), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, false)), 0.01); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); @@ -194,7 +197,7 @@ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { // given target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, givenExecutionPlan(false, false), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, false)), 0.01); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); @@ -230,7 +233,7 @@ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { void callShouldReturnResultWithoutEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() { // given target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, givenExecutionPlan(false, true), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, true)), 0.01); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); @@ -261,7 +264,7 @@ void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { // given final ModuleContext moduleContext = new ModuleContext(); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, givenExecutionPlan(true, true), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, true)), 0.01); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); @@ -299,7 +302,7 @@ void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, ExecutionPlan.empty(), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -328,7 +331,7 @@ void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { jsonMerger, givenOptableTargetingProperties("key", null, "origin", false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, ExecutionPlan.empty(), 0.01); + configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java index 3e262b0c91f..08d4faa9116 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java @@ -8,6 +8,8 @@ import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.model.Endpoint; +import org.prebid.server.settings.model.Account; +import org.prebid.server.settings.model.AccountHooksConfiguration; import java.util.List; import java.util.Map; @@ -16,35 +18,112 @@ public class CompositeHookExecutionPlanTest { + @Test + public void hasRawAuctionRequestHookShouldReturnTrueWhenGlobalPlanHasHook() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasRawAuctionRequestHook(account)).isTrue(); + } + @Test public void hasRawAuctionRequestHookShouldReturnTrueWhenAccountPlanHasHook() { // given final ExecutionPlan accountPlan = givenExecutionPlan( "raw_auction_request", "optable-targeting-raw-auction-request-hook"); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, accountPlan); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.hasRawAuctionRequestHook()).isTrue(); + assertThat(target.hasRawAuctionRequestHook(account)).isTrue(); } @Test - public void hasRawAuctionRequestHookShouldReturnTrueWhenGlobalPlanHasHook() { + public void hasRawAuctionRequestHookShouldReturnTrueWhenBothPlansHaveHook() { // given final ExecutionPlan globalPlan = givenExecutionPlan( "raw_auction_request", "optable-targeting-raw-auction-request-hook"); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan, null); + final ExecutionPlan accountPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.hasRawAuctionRequestHook()).isTrue(); + assertThat(target.hasRawAuctionRequestHook(account)).isTrue(); } @Test public void hasRawAuctionRequestHookShouldReturnFalseWhenNeitherPlanHasHook() { // given - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, null); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasRawAuctionRequestHook(account)).isFalse(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnFalseWhenAccountIsNull() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + + // when and then + assertThat(target.hasRawAuctionRequestHook(null)).isFalse(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnFalseWhenAccountIdIsEmpty() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("").build(); + + // when and then + assertThat(target.hasRawAuctionRequestHook(account)).isFalse(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnGlobalFlagWhenAccountHasNoHooksConfig() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); // when and then - assertThat(target.hasRawAuctionRequestHook()).isFalse(); + assertThat(target.hasRawAuctionRequestHook(account)).isTrue(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() { + // given + final ExecutionPlan accountPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.hasRawAuctionRequestHook(account)).isTrue(); + assertThat(target.hasRawAuctionRequestHook(account)).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldReturnTrueWhenGlobalPlanHasHook() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isTrue(); } @Test @@ -52,30 +131,107 @@ public void hasBidderRequestHookShouldReturnTrueWhenAccountPlanHasHook() { // given final ExecutionPlan accountPlan = givenExecutionPlan( "bidder_request", "optable-targeting-bidder-request-hook"); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, accountPlan); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.hasBidderRequestHook()).isTrue(); + assertThat(target.hasBidderRequestHook(account)).isTrue(); } @Test - public void hasBidderRequestHookShouldReturnTrueWhenGlobalPlanHasHook() { + public void hasBidderRequestHookShouldReturnTrueWhenBothPlansHaveHook() { // given final ExecutionPlan globalPlan = givenExecutionPlan( "bidder_request", "optable-targeting-bidder-request-hook"); - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan, null); + final ExecutionPlan accountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.hasBidderRequestHook()).isTrue(); + assertThat(target.hasBidderRequestHook(account)).isTrue(); } @Test public void hasBidderRequestHookShouldReturnFalseWhenNeitherPlanHasHook() { // given - final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null, null); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenAccountIsNull() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); // when and then - assertThat(target.hasBidderRequestHook()).isFalse(); + assertThat(target.hasBidderRequestHook(null)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenAccountIdIsEmpty() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnGlobalFlagWhenAccountHasNoHooksConfig() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isTrue(); + } + + @Test + public void hasBidderRequestHookShouldReturnSameResultOnRepeatedCallsForSameAccount() { + // given + final ExecutionPlan accountPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isTrue(); + assertThat(target.hasBidderRequestHook(account)).isTrue(); + } + + @Test + public void hasRawAuctionRequestHookShouldReturnFalseWhenOnlyBidderRequestHookIsInGlobalPlan() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "bidder_request", "optable-targeting-bidder-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasRawAuctionRequestHook(account)).isFalse(); + } + + @Test + public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIsInGlobalPlan() { + // given + final ExecutionPlan globalPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.hasBidderRequestHook(account)).isFalse(); } private ExecutionPlan givenExecutionPlan(String stage, String hookCode) { @@ -85,4 +241,12 @@ private ExecutionPlan givenExecutionPlan(String stage, String hookCode) { final EndpointExecutionPlan endpointPlan = EndpointExecutionPlan.of(Map.of(Stage.valueOf(stage), stagePlan)); return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointPlan)); } + + private Account givenAccount(String accountId, ExecutionPlan executionPlan) { + return Account.builder() + .id(accountId) + .hooks(AccountHooksConfiguration.of(executionPlan, null, null)) + .build(); + } } + From b6c11f7f1460a18dbba1e09b97cf1eb3f70387d1 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 14 May 2026 21:20:52 +0200 Subject: [PATCH 07/19] optable-targeting: code cleanup --- .../config/OptableTargetingConfig.java | 6 +- .../v1/OptableBidderRequestHook.java | 3 - .../v1/OptableRawAuctionRequestHook.java | 9 +- ...eTargetingProcessedAuctionRequestHook.java | 4 +- .../v1/core/CompositeHookExecutionPlan.java | 4 +- .../v1/OptableBidderRequestHookTest.java | 77 +++++------------ ...tableTargetingAuctionResponseHookTest.java | 19 +++-- ...getingProcessedAuctionRequestHookTest.java | 84 +++++++++---------- .../v1/core/BidderEnrichmentSamplerTest.java | 60 ++++++------- ...aml => prebid-config-with-optable_v2.yaml} | 2 +- .../configs/sample-app-settings-optable.yaml | 34 -------- ...ml => sample-app-settings-optable_v2.yaml} | 34 ++++++++ 12 files changed, 154 insertions(+), 182 deletions(-) rename sample/configs/{prebid-config-with-optable-old.yaml => prebid-config-with-optable_v2.yaml} (98%) rename sample/configs/{sample-app-settings-optable-old.yaml => sample-app-settings-optable_v2.yaml} (59%) 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 1870e8f41f5..9f28975a03d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -55,12 +55,12 @@ APIClientImpl apiClient(HttpClient httpClient, @Value("${logging.sampling-rate:0.01}") double logSamplingRate, OptableTargetingProperties optableTargetingProperties, - JacksonMapper jacksonMapperr) { + JacksonMapper jacksonMapper) { return new APIClientImpl( optableTargetingProperties.getApiEndpoint(), httpClient, - jacksonMapperr, + jacksonMapper, logSamplingRate); } @@ -106,7 +106,7 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, JsonMerger jsonMerger, BidderCatalog bidderCatalog, JacksonMapper mapper, - @Value("${hooks.host-execution-plan}") + @Value("${hooks.host-execution-plan:}") String executionPlan, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index e7f741976ad..467124c1286 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java @@ -7,7 +7,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderRequestEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -60,7 +59,6 @@ private Future> noAction(ModuleContext mo InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.no_action) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) .moduleContext(moduleContext) .build()); } @@ -73,7 +71,6 @@ private static Future> update( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) .build()); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index bd31576ac34..d03982d559e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -7,7 +7,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -97,20 +96,18 @@ private static Future> updateModuleConte InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.no_action) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) .moduleContext(moduleContext) .build()); } - public static Future> update( - PayloadUpdate payloadUpdate, + public static Future> update( + PayloadUpdate payloadUpdate, ModuleContext moduleContext) { return Future.succeededFuture( - InvocationResultImpl.builder() + InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) - .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) .payloadUpdate(payloadUpdate) .moduleContext(moduleContext) .build()); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 5d463d67fca..64fa23fc39e 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -29,7 +29,7 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( - LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); + LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); public static final String CODE = "optable-targeting-processed-auction-request-hook"; @@ -40,7 +40,7 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc private final NetworkCall networkCall; private final double logSamplingRate; - final CompositeHookExecutionPlan hooksExecutionPlan; + private final CompositeHookExecutionPlan hooksExecutionPlan; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java index 814321cc6be..ffd9b0b00a8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java @@ -46,9 +46,9 @@ public boolean hasRawAuctionRequestHook(Account account) { return StringUtils.isNotEmpty(accountId) ? rawAuctionRequestHookCache.computeIfAbsent(accountId, id -> { - final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account); + final ExecutionPlan accountSpecificHooksExecutionPlan = resolveExecutionPlan(account); return hasHook( - accountSpecificHoksExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) + accountSpecificHooksExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) || hasGlobalRawAuctionRequestHook; }) : false; diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java index c68e58bb5a6..ac14a7dc3eb 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -10,7 +10,9 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.prebid.server.hooks.execution.v1.bidder.BidderRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -62,8 +64,9 @@ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() { // then assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); assertThat(result.moduleContext()).isSameAs(moduleContext); } @@ -82,8 +85,9 @@ public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() { // then assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); } @Test @@ -100,8 +104,9 @@ public void shouldReturnNoActionWhenBiddersToEnrichIsNull() { // then assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.no_action); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); } @Test @@ -120,9 +125,10 @@ public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() { // then assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final BidRequest enrichedRequest = result .payloadUpdate() @@ -146,10 +152,12 @@ public void shouldUpdateModuleContextWithTargetingOnSuccess() { // when target.call(bidderRequestPayload, invocationContext); - // then — the moduleContext is mutated in-place with audience targeting + // then assertThat(moduleContext.getTargeting()).isNotNull().isNotEmpty(); - assertThat(moduleContext.getEnrichRequestStatus()).isNotNull(); - assertThat(moduleContext.getEnrichRequestStatus().getStatus().getValue()).isEqualTo("success"); + assertThat(moduleContext.getEnrichRequestStatus()).isNotNull() + .extracting(EnrichmentStatus::getStatus) + .extracting(Status::getValue) + .isEqualTo("success"); } @Test @@ -169,48 +177,9 @@ public void shouldReturnNoActionWhenTargetingCallFails() { // then assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.no_action); - } - - @Test - public void shouldIncludeAnalyticsTagsInNoActionResponse() { - // given - final ModuleContext moduleContext = givenModuleContextWithProperties( - givenOptableTargetingProperties(false)); - when(invocationContext.moduleContext()).thenReturn(moduleContext); - - // when - final Future> future = - target.call(bidderRequestPayload, invocationContext); - - // then - final InvocationResult result = future.result(); - assertThat(result.analyticsTags()).isNotNull(); - assertThat(result.analyticsTags().activities()).isNotEmpty(); - assertThat(result.analyticsTags().activities().getFirst().name()) - .isEqualTo("optable-enrich-request"); - } - - @Test - public void shouldIncludeAnalyticsTagsInUpdateResponse() { - // given - final ModuleContext moduleContext = givenModuleContextWithProperties( - givenPropertiesWithPerBidderEnrichmentEnabled()); - moduleContext.setBiddersToEnrich(Set.of("bidder1")); - moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); - when(invocationContext.moduleContext()).thenReturn(moduleContext); - - // when - final Future> future = - target.call(bidderRequestPayload, invocationContext); - - // then - final InvocationResult result = future.result(); - assertThat(result.analyticsTags()).isNotNull(); - assertThat(result.analyticsTags().activities()).isNotEmpty(); - assertThat(result.analyticsTags().activities().getFirst().name()) - .isEqualTo("optable-enrich-request"); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); } private static ModuleContext givenModuleContextWithProperties(OptableTargetingProperties properties) { 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 cf67cfdad0a..7d1df51bc5e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -16,6 +16,9 @@ import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.analytics.Activity; +import org.prebid.server.hooks.v1.analytics.Result; +import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionResponseHook; import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; @@ -68,11 +71,17 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.no_action); - assertThat(result.analyticsTags().activities().getFirst() - .results().getFirst().values().get("reason")).isNotNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(result.analyticsTags()) + .extracting(Tags::activities) + .extracting(List::getFirst) + .extracting(Activity::results) + .extracting(List::getFirst) + .extracting(Result::values) + .extracting(it -> it.get("reason")) + .isNotNull(); assertThat(result.errors()).isNull(); } 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 c1082422150..bd5b8a25d92 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -110,10 +110,10 @@ void callShouldReturnResultWithPBSAnalyticsTags() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); assertThat(result.analyticsTags().activities().getFirst() .results().getFirst().values().get("execution-time")).isNotNull(); } @@ -134,10 +134,10 @@ void callShouldReturnResultWithUpdateActionWhenOptableTargetingReturnsTargeting( assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final BidRequest bidRequest = result .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) @@ -175,10 +175,10 @@ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final BidRequest bidRequest = result .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) @@ -211,10 +211,10 @@ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final BidRequest bidRequest = result .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) @@ -247,10 +247,10 @@ void callShouldReturnResultWithoutEnrichedBidRequestWhenOnlyBidderRequestHookIsP assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final BidRequest bidRequest = result .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) @@ -282,10 +282,10 @@ void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final BidRequest bidRequest = result .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) @@ -315,9 +315,9 @@ void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); assertThat((ModuleContext) result.moduleContext()) .extracting(it -> it.getEnrichRequestStatus().getStatus()) .isEqualTo(Status.FAIL); @@ -344,9 +344,9 @@ void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); assertThat((ModuleContext) result.moduleContext()) .extracting(it -> it.getEnrichRequestStatus().getStatus()) .isEqualTo(Status.FAIL); @@ -368,10 +368,10 @@ void callShouldReturnResultWithCleanedUpUserExtOptableTag() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); final ObjectNode optable = (ObjectNode) result .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) @@ -396,10 +396,10 @@ void callShouldReturnResultWithUpdateWhenOptableTargetingDoesNotReturnResult() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); } @Test @@ -418,10 +418,10 @@ void callShouldReturnUpdateWhenTrafficSourceIsInvalid() { assertThat(future.succeeded()).isTrue(); final InvocationResult result = future.result(); - assertThat(result).isNotNull(); - assertThat(result.status()).isEqualTo(InvocationStatus.success); - assertThat(result.action()).isEqualTo(InvocationAction.update); - assertThat(result.errors()).isNull(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action) + .extracting(InvocationResult::errors).isNull(); } private ObjectNode givenAccountConfig(boolean cacheEnabled) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java index 27220fd9b1d..bfcc3057b76 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java @@ -45,59 +45,59 @@ public void setUp() { } @Test - public void diceShouldReturnEmptySetWhenRequestHasNoImpressions() { + public void sampleShouldReturnEmptySetWhenRequestHasNoImpressions() { // given final BidRequest bidRequest = givenBidRequest(identity()); // when - final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldReturnEmptySetWhenImpHasNoExt() { + public void sampleShouldReturnEmptySetWhenImpHasNoExt() { // given final BidRequest bidRequest = givenBidRequest( request -> request.imp(List.of(givenImp(identity())))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldReturnEmptySetWhenImpExtHasNoPrebidBidderNode() { + public void sampleShouldReturnEmptySetWhenImpExtHasNoPrebidBidderNode() { // given final ObjectNode ext = mapper.createObjectNode(); final BidRequest bidRequest = givenBidRequest( request -> request.imp(List.of(givenImp(imp -> imp.ext(ext))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldReturnEmptySetWhenBidderNodeIsEmpty() { + public void sampleShouldReturnEmptySetWhenBidderNodeIsEmpty() { // given final BidRequest bidRequest = givenBidRequest( request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt()))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldIncludeAllBiddersWhenDefaultPercentageIs100() { + public void sampleShouldIncludeAllBiddersWhenDefaultPercentageIs100() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(99); @@ -106,14 +106,14 @@ public void diceShouldIncludeAllBiddersWhenDefaultPercentageIs100() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); // then assertThat(result).containsExactlyInAnyOrder("bidderA", "bidderB"); } @Test - public void diceShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() { + public void sampleShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(0); @@ -122,14 +122,14 @@ public void diceShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(-1, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(-1, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldIncludeBidderWhenRandomValueEqualsPercentage() { + public void sampleShouldIncludeBidderWhenRandomValueEqualsPercentage() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(50); @@ -138,14 +138,14 @@ public void diceShouldIncludeBidderWhenRandomValueEqualsPercentage() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(50, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap())); // then assertThat(result).containsExactly("bidderA"); } @Test - public void diceShouldIncludeBidderWhenRandomValueIsBelowPercentage() { + public void sampleShouldIncludeBidderWhenRandomValueIsBelowPercentage() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(49); @@ -154,14 +154,14 @@ public void diceShouldIncludeBidderWhenRandomValueIsBelowPercentage() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(50, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap())); // then assertThat(result).containsExactly("bidderA"); } @Test - public void diceShouldExcludeBidderWhenRandomValueExceedsPercentage() { + public void sampleShouldExcludeBidderWhenRandomValueExceedsPercentage() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(51); @@ -170,15 +170,15 @@ public void diceShouldExcludeBidderWhenRandomValueExceedsPercentage() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(50, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldIncludeBidderWhenPercentageIsZeroAndRandomIsZero() { - // given — 0% still includes exactly when random == 0 + public void sampleShouldIncludeBidderWhenPercentageIsZeroAndRandomIsZero() { + // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(0); @@ -186,14 +186,14 @@ public void diceShouldIncludeBidderWhenPercentageIsZeroAndRandomIsZero() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(0, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(0, Collections.emptyMap())); // then assertThat(result).containsExactly("bidderA"); } @Test - public void diceShouldExcludeBidderWhenPercentageIsZeroAndRandomIsOne() { + public void sampleShouldExcludeBidderWhenPercentageIsZeroAndRandomIsOne() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(1); @@ -202,14 +202,14 @@ public void diceShouldExcludeBidderWhenPercentageIsZeroAndRandomIsOne() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(0, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(0, Collections.emptyMap())); // then assertThat(result).isEmpty(); } @Test - public void diceShouldUseBidderSpecificPercentageWhenAvailable() { + public void sampleShouldUseBidderSpecificPercentageWhenAvailable() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(99); @@ -218,14 +218,14 @@ public void diceShouldUseBidderSpecificPercentageWhenAvailable() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(-1, Map.of("bidderA", 100))); + final Set result = target.sample(bidRequest, givenSampleProperties(-1, Map.of("bidderA", 100))); // then assertThat(result).containsExactly("bidderA"); } @Test - public void diceShouldUseAliasSpecificPercentageWhenBidderResolvesToAlias() { + public void sampleShouldUseAliasSpecificPercentageWhenBidderResolvesToAlias() { // given given(bidderAliases.resolveBidder("bidderA")).willReturn("bidderA"); given(bidderAliases.resolveBidder("bidderB")).willReturn("aliasB"); @@ -235,14 +235,14 @@ public void diceShouldUseAliasSpecificPercentageWhenBidderResolvesToAlias() { request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(-1, Map.of("aliasB", 100))); + final Set result = target.sample(bidRequest, givenSampleProperties(-1, Map.of("aliasB", 100))); // then assertThat(result).containsExactly("bidderB"); } @Test - public void diceShouldDeduplicateBiddersAppearingInMultipleImps() { + public void sampleShouldDeduplicateBiddersAppearingInMultipleImps() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(0); @@ -253,13 +253,13 @@ public void diceShouldDeduplicateBiddersAppearingInMultipleImps() { givenImp(imp -> imp.ext(ext))))); // when - final Set result = target.sample(bidRequest, givenDiceProperties(100, Collections.emptyMap())); + final Set result = target.sample(bidRequest, givenSampleProperties(100, Collections.emptyMap())); // then assertThat(result).containsExactly("bidderA"); } - private OptableTargetingProperties givenDiceProperties(int defaultPct, Map bidderPcts) { + private OptableTargetingProperties givenSampleProperties(int defaultPct, Map bidderPcts) { final OptableTargetingProperties props = new OptableTargetingProperties(); props.setEnrichmentPercentage(defaultPct); props.setBidderEnrichmentPercentages(bidderPcts); diff --git a/sample/configs/prebid-config-with-optable-old.yaml b/sample/configs/prebid-config-with-optable_v2.yaml similarity index 98% rename from sample/configs/prebid-config-with-optable-old.yaml rename to sample/configs/prebid-config-with-optable_v2.yaml index 9efe5a34e5c..1d30adf7992 100644 --- a/sample/configs/prebid-config-with-optable-old.yaml +++ b/sample/configs/prebid-config-with-optable_v2.yaml @@ -27,7 +27,7 @@ settings: enforce-valid-account: false generate-storedrequest-bidrequest-id: true filesystem: - settings-filename: sample/configs/sample-app-settings-optable-old.yaml + settings-filename: sample/configs/sample-app-settings-optable_v2.yaml stored-requests-dir: sample stored-imps-dir: sample stored-responses-dir: sample/stored diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 5bb0d589483..7a533da3697 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -21,14 +21,6 @@ accounts: api-key: key tenant: optable origin: web-sdk-demo - enrichment-percentage: 100 - bidder-enrichment-percentages: - appnexus: 75 - rubicon: 75 - pubmatic: 100 - criteo: 0 - enrich-web: true - enrich-app: true ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: @@ -39,32 +31,6 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { - "raw-auction-request": { - "groups": [ - { - "timeout": 1000, - "hook-sequence": [ - { - "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-raw-auction-request-hook" - } - ] - } - ] - }, - "bidder-request": { - "groups": [ - { - "timeout": 500, - "hook-sequence": [ - { - "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-bidder-request-hook" - } - ] - } - ] - }, "processed-auction-request": { "groups": [ { diff --git a/sample/configs/sample-app-settings-optable-old.yaml b/sample/configs/sample-app-settings-optable_v2.yaml similarity index 59% rename from sample/configs/sample-app-settings-optable-old.yaml rename to sample/configs/sample-app-settings-optable_v2.yaml index 7a533da3697..5bb0d589483 100644 --- a/sample/configs/sample-app-settings-optable-old.yaml +++ b/sample/configs/sample-app-settings-optable_v2.yaml @@ -21,6 +21,14 @@ accounts: api-key: key tenant: optable origin: web-sdk-demo + enrichment-percentage: 100 + bidder-enrichment-percentages: + appnexus: 75 + rubicon: 75 + pubmatic: 100 + criteo: 0 + enrich-web: true + enrich-app: true ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: @@ -31,6 +39,32 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { + "raw-auction-request": { + "groups": [ + { + "timeout": 1000, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, + "bidder-request": { + "groups": [ + { + "timeout": 500, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-bidder-request-hook" + } + ] + } + ] + }, "processed-auction-request": { "groups": [ { From ed24621dbb8fc47ad279b6ba2de4e3edd7346cc1 Mon Sep 17 00:00:00 2001 From: softcoder Date: Sat, 23 May 2026 21:32:37 +0200 Subject: [PATCH 08/19] optable-targeting: update timeouts --- .../config/OptableTargetingConfig.java | 9 +- .../v1/OptableRawAuctionRequestHook.java | 14 ++- ...eTargetingProcessedAuctionRequestHook.java | 4 +- .../v1/core/CompositeHookExecutionPlan.java | 54 ++++++++- .../targeting/v1/core/NetworkCall.java | 17 ++- .../v1/OptableRawAuctionRequestHookTest.java | 19 ++- ...getingProcessedAuctionRequestHookTest.java | 10 +- .../core/CompositeHookExecutionPlanTest.java | 108 ++++++++++++++++++ .../sample-app-settings-optable_v2.yaml | 4 +- 9 files changed, 215 insertions(+), 24 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 9f28975a03d..ceeb947c3ec 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 @@ -5,6 +5,7 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.bidder.BidderCatalog; import org.prebid.server.cache.PbcStorageService; +import org.prebid.server.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.model.ExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook; @@ -96,8 +97,11 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties } @Bean - NetworkCall networkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) { - return new NetworkCall(optableTargeting, userFpdActivityMask); + NetworkCall networkCall(OptableTargeting optableTargeting, + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory) { + + return new NetworkCall(optableTargeting, userFpdActivityMask, timeoutFactory); } @Bean @@ -120,6 +124,7 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, configResolver, networkCall, BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog)), + hooksExecutionPlan, logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index d03982d559e..3ff76bedb58 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -9,6 +9,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; @@ -21,6 +22,7 @@ import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; import org.prebid.server.log.ConditionalLogger; import org.prebid.server.log.LoggerFactory; +import org.prebid.server.settings.model.Account; import java.util.Objects; import java.util.Set; @@ -30,21 +32,24 @@ public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); - private static final String CODE = "optable-targeting-raw-auction-request-hook"; + public static final String CODE = "optable-targeting-raw-auction-request-hook"; private final ConfigResolver configResolver; private final NetworkCall networkCall; private final BidderEnrichmentSampler bidderEnrichmentSampler; + private final CompositeHookExecutionPlan hooksExecutionPlan; private final double logSamplingRate; public OptableRawAuctionRequestHook(ConfigResolver configResolver, NetworkCall networkCall, BidderEnrichmentSampler bidderEnrichmentSampler, + CompositeHookExecutionPlan hooksExecutionPlan, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); this.networkCall = Objects.requireNonNull(networkCall); this.bidderEnrichmentSampler = Objects.requireNonNull(bidderEnrichmentSampler); + this.hooksExecutionPlan = hooksExecutionPlan; this.logSamplingRate = logSamplingRate; } @@ -80,10 +85,15 @@ public Future> call(AuctionRequestPayloa } moduleContext.setBiddersToEnrich(biddersToEnrich); + final Account account = invocationContext.auctionContext().getAccount(); + final long processedAuctionHookTimeout = + hooksExecutionPlan.getOptableTargetingProcessedAuctionRequestTimeout(account); + final Future optableTargetingCall = networkCall.makeRequest( payload, invocationContext, - properties); + properties, + processedAuctionHookTimeout); moduleContext.setOptableTargetingCall(optableTargetingCall); 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 64fa23fc39e..c3b5da2f4ad 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 @@ -66,7 +66,6 @@ public Future> call(AuctionRequestPayloa final Account account = invocationContext.auctionContext().getAccount(); final boolean hasRawAuctionRequestHook = hooksExecutionPlan.hasRawAuctionRequestHook(account); final boolean hasBidderRequestHook = hooksExecutionPlan.hasBidderRequestHook(account); - final OptableTargetingProperties properties = resolveOptableTargetingProperties(moduleContext, invocationContext); @@ -143,7 +142,8 @@ private Future resolvePreEarlyNetworkCall( return networkCall.makeRequest( payload, invocationContext, - properties); + properties, + null); } private static Future> update( diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java index ffd9b0b00a8..907df81e6cd 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java @@ -6,6 +6,9 @@ import org.prebid.server.hooks.execution.model.ExecutionPlan; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; @@ -18,27 +21,40 @@ public class CompositeHookExecutionPlan { private static final String ENDPOINT_AUCTION = "openrtb2_auction"; private static final String STAGE_RAW_AUCTION_REQUEST = "raw_auction_request"; private static final String STAGE_BIDDER_REQUEST = "bidder_request"; - private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = "optable-targeting-raw-auction-request-hook"; - private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = "optable-targeting-bidder-request-hook"; + private static final String STAGE_PROCESSED_AUCTION_REQUEST = "processed_auction_request"; + private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = OptableRawAuctionRequestHook.CODE; + private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = OptableBidderRequestHook.CODE; + private static final String HOOK_CODE_OPTABLE_PROCESSED_AUCTION_REQUEST = + OptableTargetingProcessedAuctionRequestHook.CODE; private final boolean hasGlobalRawAuctionRequestHook; private final boolean hasGlobalBidderRequestHook; + private final long globalProcessedAuctionRequestHookTimeout; + private final ConcurrentHashMap rawAuctionRequestHookCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>(); - private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook, boolean hasGlobalBidderRequestHook) { + private final ConcurrentHashMap processedAuctionRequestHookTimeoutCache = new ConcurrentHashMap<>(); + + private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook, + boolean hasGlobalBidderRequestHook, + long globalProcessedAuctionRequestHookTimeout) { + this.hasGlobalRawAuctionRequestHook = hasGlobalRawAuctionRequestHook; this.hasGlobalBidderRequestHook = hasGlobalBidderRequestHook; + this.globalProcessedAuctionRequestHookTimeout = globalProcessedAuctionRequestHookTimeout; } public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) { return globalExecutionPlan == null - ? new CompositeHookExecutionPlan(false, false) + ? new CompositeHookExecutionPlan(false, false, 0) : new CompositeHookExecutionPlan( hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION), - hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)); + hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST), + getHookTimeout(globalExecutionPlan, + STAGE_PROCESSED_AUCTION_REQUEST, HOOK_CODE_OPTABLE_PROCESSED_AUCTION_REQUEST)); } public boolean hasRawAuctionRequestHook(Account account) { @@ -67,6 +83,21 @@ public boolean hasBidderRequestHook(Account account) { : false; } + public long getOptableTargetingProcessedAuctionRequestTimeout(Account account) { + final String accountId = account != null ? account.getId() : null; + + return StringUtils.isNotEmpty(accountId) + ? processedAuctionRequestHookTimeoutCache.computeIfAbsent(accountId, id -> { + final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account); + final long hookTimeOut = getHookTimeout( + accountSpecificHoksExecutionPlan, + STAGE_PROCESSED_AUCTION_REQUEST, + HOOK_CODE_OPTABLE_PROCESSED_AUCTION_REQUEST); + return hookTimeOut != 0 ? hookTimeOut : globalProcessedAuctionRequestHookTimeout; + }) + : globalProcessedAuctionRequestHookTimeout; + } + private ExecutionPlan resolveExecutionPlan(Account account) { return Optional.ofNullable(account) .map(org.prebid.server.settings.model.Account::getHooks) @@ -87,4 +118,17 @@ private static boolean hasHook(ExecutionPlan executionPlan, String stage, String .flatMap(java.util.Collection::stream) .anyMatch(hook -> hookCode.equals(hook.getHookImplCode())); } + + private static long getHookTimeout(ExecutionPlan executionPlan, String stage, String hookCode) { + return Optional.ofNullable(executionPlan) + .map(ExecutionPlan::getEndpoints) + .map(endpoints -> endpoints.get(Endpoint.valueOf(ENDPOINT_AUCTION))) + .map(EndpointExecutionPlan::getStages) + .map(stages -> stages.get(Stage.valueOf(stage))) + .map(StageExecutionPlan::getGroups) + .orElseGet(List::of) + .stream().findFirst() + .map(ExecutionGroup::getTimeout) + .orElse(0L); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java index be7d2c42acb..967f9d36cea 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java @@ -4,6 +4,7 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.User; import io.vertx.core.Future; +import org.apache.commons.lang3.ObjectUtils; import org.prebid.server.activity.Activity; import org.prebid.server.activity.ComponentType; import org.prebid.server.activity.infrastructure.ActivityInfrastructure; @@ -13,6 +14,7 @@ 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.execution.timeout.TimeoutFactory; 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; @@ -26,20 +28,27 @@ public class NetworkCall { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; + private final TimeoutFactory timeoutFactory; - public NetworkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) { + public NetworkCall(OptableTargeting optableTargeting, + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory) { this.optableTargeting = Objects.requireNonNull(optableTargeting); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.timeoutFactory = ObjectUtils.requireNonEmpty(timeoutFactory); } public Future makeRequest(AuctionRequestPayload payload, - AuctionInvocationContext invocationContext, - OptableTargetingProperties properties) { + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties, + Long apiTimeout) { final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext); - final Timeout timeout = getHookTimeout(invocationContext); + final Timeout timeout = apiTimeout == null + ? getHookTimeout(invocationContext) + : timeoutFactory.create(getHookTimeout(invocationContext).remaining() + apiTimeout); final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), properties.getTimeout()); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java index f5e6a39663e..4da8ef24068 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -14,8 +14,11 @@ 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.execution.timeout.TimeoutFactory; +import org.prebid.server.hooks.execution.model.ExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderEnrichmentSampler; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; @@ -47,6 +50,8 @@ public class OptableRawAuctionRequestHookTest extends BaseOptableTest { @Mock private Timeout timeout; @Mock + private TimeoutFactory timeoutFactory; + @Mock private BidderEnrichmentSampler bidderEnrichmentSampler; private ConfigResolver configResolver; @@ -58,8 +63,10 @@ public void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01); + networkCall = new NetworkCall(optableTargeting, userFpdActivityMask, timeoutFactory); + target = new OptableRawAuctionRequestHook( + configResolver, networkCall, bidderEnrichmentSampler, + CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); when(invocationContext.timeout()).thenReturn(timeout); when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); @@ -113,7 +120,9 @@ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAc configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true)); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01); + target = new OptableRawAuctionRequestHook( + configResolver, networkCall, bidderEnrichmentSampler, + CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); // when final Future> result = @@ -148,7 +157,9 @@ public void shouldNotInjectEarlyNetworkCallWhenTrafficSourceIsInvalid(VertxTestC .build()); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - target = new OptableRawAuctionRequestHook(configResolver, networkCall, bidderEnrichmentSampler, 0.01); + target = new OptableRawAuctionRequestHook( + configResolver, networkCall, bidderEnrichmentSampler, + CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); // when final Future> result = 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 bd5b8a25d92..1b8b8f9cbd5 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 @@ -15,6 +15,7 @@ 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.execution.timeout.TimeoutFactory; import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; @@ -67,6 +68,9 @@ class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { @Mock(strictness = Mock.Strictness.LENIENT) private Timeout timeout; + @Mock(strictness = Mock.Strictness.LENIENT) + private TimeoutFactory timeoutFactory; + private NetworkCall networkCall; private OptableTargetingProcessedAuctionRequestHook target; @@ -76,7 +80,7 @@ void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); + networkCall = new NetworkCall(optableTargeting, userFpdActivityMask, timeoutFactory); target = new OptableTargetingProcessedAuctionRequestHook( configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); @@ -164,7 +168,7 @@ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); moduleContext.setOptableTargetingCall( networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( - "key", "tenant", "origin", false))); + "key", "tenant", "origin", false), null)); // when final Future> future = target.call(auctionRequestPayload, @@ -271,7 +275,7 @@ void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); moduleContext.setOptableTargetingCall( networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( - "key", "tenant", "origin", false))); + "key", "tenant", "origin", false), 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/CompositeHookExecutionPlanTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java index 08d4faa9116..f5f5d72f867 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java @@ -234,6 +234,106 @@ public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIs assertThat(target.hasBidderRequestHook(account)).isFalse(); } + @Test + public void getProcessedAuctionRequestTimeoutShouldReturnGlobalTimeoutWhenConfigured() { + // given + final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 500L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(500L); + } + + @Test + public void getProcessedAuctionRequestTimeoutShouldReturnAccountTimeoutWhenAccountPlanOverrides() { + // given + final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 500L); + final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 200L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(200L); + } + + @Test + public void getProcessedAuctionRequestTimeoutShouldFallbackToGlobalWhenAccountPlanHasNoTimeout() { + // given + final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 300L); + final ExecutionPlan accountPlan = givenExecutionPlan( + "raw_auction_request", "optable-targeting-raw-auction-request-hook"); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(300L); + } + + @Test + public void getProcessedAuctionRequestTimeoutShouldReturnZeroWhenNoPlanIsConfigured() { + // given + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = Account.builder().id("accountId").build(); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(0L); + } + + @Test + public void getProcessedAuctionRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIsNull() { + // given + final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 400L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(null)).isEqualTo(400L); + } + + @Test + public void getProcessedAuctionRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIdIsEmpty() { + // given + final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 150L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); + final Account account = Account.builder().id("").build(); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(150L); + } + + @Test + public void getProcessedAuctionRequestTimeoutShouldReturnSameResultOnRepeatedCallsForSameAccount() { + // given + final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout( + "processed_auction_request", + "optable-targeting-processed-auction-request-hook", + 250L); + final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); + final Account account = givenAccount("accountId", accountPlan); + + // when and then + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(250L); + assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(250L); + } + private ExecutionPlan givenExecutionPlan(String stage, String hookCode) { final HookId hookId = HookId.of("optable-targeting", hookCode); final ExecutionGroup group = ExecutionGroup.of(null, List.of(hookId)); @@ -242,6 +342,14 @@ private ExecutionPlan givenExecutionPlan(String stage, String hookCode) { return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointPlan)); } + private ExecutionPlan givenExecutionPlanWithTimeout(String stage, String hookCode, long timeout) { + final HookId hookId = HookId.of("optable-targeting", hookCode); + final ExecutionGroup group = ExecutionGroup.of(timeout, List.of(hookId)); + final StageExecutionPlan stagePlan = StageExecutionPlan.of(List.of(group)); + final EndpointExecutionPlan endpointPlan = EndpointExecutionPlan.of(Map.of(Stage.valueOf(stage), stagePlan)); + return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointPlan)); + } + private Account givenAccount(String accountId, ExecutionPlan executionPlan) { return Account.builder() .id(accountId) diff --git a/sample/configs/sample-app-settings-optable_v2.yaml b/sample/configs/sample-app-settings-optable_v2.yaml index 5bb0d589483..811d129eaf4 100644 --- a/sample/configs/sample-app-settings-optable_v2.yaml +++ b/sample/configs/sample-app-settings-optable_v2.yaml @@ -42,7 +42,7 @@ accounts: "raw-auction-request": { "groups": [ { - "timeout": 1000, + "timeout": 50, "hook-sequence": [ { "module-code": "optable-targeting", @@ -55,7 +55,7 @@ accounts: "bidder-request": { "groups": [ { - "timeout": 500, + "timeout": 50, "hook-sequence": [ { "module-code": "optable-targeting", From 33943ca99fe38f12d218c80585cbff45c15a96c0 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 20:01:20 +0200 Subject: [PATCH 09/19] extract timeout for the future from bidder-request --- .../v1/OptableRawAuctionRequestHook.java | 6 +- .../v1/core/CompositeHookExecutionPlan.java | 26 ++++----- .../core/CompositeHookExecutionPlanTest.java | 58 +++++++++---------- 3 files changed, 43 insertions(+), 47 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index 3ff76bedb58..1c53f7ea4e9 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -86,14 +86,14 @@ public Future> call(AuctionRequestPayloa moduleContext.setBiddersToEnrich(biddersToEnrich); final Account account = invocationContext.auctionContext().getAccount(); - final long processedAuctionHookTimeout = - hooksExecutionPlan.getOptableTargetingProcessedAuctionRequestTimeout(account); + final long crossHookFutureTimeout = + hooksExecutionPlan.getOptableTargetingBidderRequestTimeout(account); final Future optableTargetingCall = networkCall.makeRequest( payload, invocationContext, properties, - processedAuctionHookTimeout); + crossHookFutureTimeout); moduleContext.setOptableTargetingCall(optableTargetingCall); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java index 907df81e6cd..280e9b2192d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlan.java @@ -8,7 +8,6 @@ import org.prebid.server.hooks.execution.model.StageExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableBidderRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook; -import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; @@ -21,30 +20,27 @@ public class CompositeHookExecutionPlan { private static final String ENDPOINT_AUCTION = "openrtb2_auction"; private static final String STAGE_RAW_AUCTION_REQUEST = "raw_auction_request"; private static final String STAGE_BIDDER_REQUEST = "bidder_request"; - private static final String STAGE_PROCESSED_AUCTION_REQUEST = "processed_auction_request"; private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = OptableRawAuctionRequestHook.CODE; private static final String HOOK_CODE_OPTABLE_BIDDER_REQUEST = OptableBidderRequestHook.CODE; - private static final String HOOK_CODE_OPTABLE_PROCESSED_AUCTION_REQUEST = - OptableTargetingProcessedAuctionRequestHook.CODE; private final boolean hasGlobalRawAuctionRequestHook; private final boolean hasGlobalBidderRequestHook; - private final long globalProcessedAuctionRequestHookTimeout; + private final long globalBidderRequestHookTimeout; private final ConcurrentHashMap rawAuctionRequestHookCache = new ConcurrentHashMap<>(); private final ConcurrentHashMap bidderRequestHookCache = new ConcurrentHashMap<>(); - private final ConcurrentHashMap processedAuctionRequestHookTimeoutCache = new ConcurrentHashMap<>(); + private final ConcurrentHashMap bidderRequestHookTimeoutCache = new ConcurrentHashMap<>(); private CompositeHookExecutionPlan(boolean hasGlobalRawAuctionRequestHook, boolean hasGlobalBidderRequestHook, - long globalProcessedAuctionRequestHookTimeout) { + long globalBidderRequestHookTimeout) { this.hasGlobalRawAuctionRequestHook = hasGlobalRawAuctionRequestHook; this.hasGlobalBidderRequestHook = hasGlobalBidderRequestHook; - this.globalProcessedAuctionRequestHookTimeout = globalProcessedAuctionRequestHookTimeout; + this.globalBidderRequestHookTimeout = globalBidderRequestHookTimeout; } public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) { @@ -54,7 +50,7 @@ public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) { hasHook(globalExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION), hasHook(globalExecutionPlan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST), getHookTimeout(globalExecutionPlan, - STAGE_PROCESSED_AUCTION_REQUEST, HOOK_CODE_OPTABLE_PROCESSED_AUCTION_REQUEST)); + STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)); } public boolean hasRawAuctionRequestHook(Account account) { @@ -83,19 +79,19 @@ public boolean hasBidderRequestHook(Account account) { : false; } - public long getOptableTargetingProcessedAuctionRequestTimeout(Account account) { + public long getOptableTargetingBidderRequestTimeout(Account account) { final String accountId = account != null ? account.getId() : null; return StringUtils.isNotEmpty(accountId) - ? processedAuctionRequestHookTimeoutCache.computeIfAbsent(accountId, id -> { + ? bidderRequestHookTimeoutCache.computeIfAbsent(accountId, id -> { final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account); final long hookTimeOut = getHookTimeout( accountSpecificHoksExecutionPlan, - STAGE_PROCESSED_AUCTION_REQUEST, - HOOK_CODE_OPTABLE_PROCESSED_AUCTION_REQUEST); - return hookTimeOut != 0 ? hookTimeOut : globalProcessedAuctionRequestHookTimeout; + STAGE_BIDDER_REQUEST, + HOOK_CODE_OPTABLE_BIDDER_REQUEST); + return hookTimeOut != 0 ? hookTimeOut : globalBidderRequestHookTimeout; }) - : globalProcessedAuctionRequestHookTimeout; + : globalBidderRequestHookTimeout; } private ExecutionPlan resolveExecutionPlan(Account account) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java index f5f5d72f867..975ecf37ccf 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CompositeHookExecutionPlanTest.java @@ -235,43 +235,43 @@ public void hasBidderRequestHookShouldReturnFalseWhenOnlyRawAuctionRequestHookIs } @Test - public void getProcessedAuctionRequestTimeoutShouldReturnGlobalTimeoutWhenConfigured() { + public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenConfigured() { // given final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 500L); final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); final Account account = Account.builder().id("accountId").build(); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(500L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(500L); } @Test - public void getProcessedAuctionRequestTimeoutShouldReturnAccountTimeoutWhenAccountPlanOverrides() { + public void getBidderRequestTimeoutShouldReturnAccountTimeoutWhenAccountPlanOverrides() { // given final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 500L); final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 200L); final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(200L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(200L); } @Test - public void getProcessedAuctionRequestTimeoutShouldFallbackToGlobalWhenAccountPlanHasNoTimeout() { + public void getBidderRequestTimeoutShouldFallbackToGlobalWhenAccountPlanHasNoTimeout() { // given final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 300L); final ExecutionPlan accountPlan = givenExecutionPlan( "raw_auction_request", "optable-targeting-raw-auction-request-hook"); @@ -279,59 +279,59 @@ public void getProcessedAuctionRequestTimeoutShouldFallbackToGlobalWhenAccountPl final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(300L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(300L); } @Test - public void getProcessedAuctionRequestTimeoutShouldReturnZeroWhenNoPlanIsConfigured() { + public void getBidderRequestTimeoutShouldReturnZeroWhenNoPlanIsConfigured() { // given final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); final Account account = Account.builder().id("accountId").build(); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(0L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(0L); } @Test - public void getProcessedAuctionRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIsNull() { + public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIsNull() { // given final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 400L); final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(null)).isEqualTo(400L); + assertThat(target.getOptableTargetingBidderRequestTimeout(null)).isEqualTo(400L); } @Test - public void getProcessedAuctionRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIdIsEmpty() { + public void getBidderRequestTimeoutShouldReturnGlobalTimeoutWhenAccountIdIsEmpty() { // given final ExecutionPlan globalPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 150L); final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(globalPlan); final Account account = Account.builder().id("").build(); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(150L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(150L); } @Test - public void getProcessedAuctionRequestTimeoutShouldReturnSameResultOnRepeatedCallsForSameAccount() { + public void getBidderRequestTimeoutShouldReturnSameResultOnRepeatedCallsForSameAccount() { // given final ExecutionPlan accountPlan = givenExecutionPlanWithTimeout( - "processed_auction_request", - "optable-targeting-processed-auction-request-hook", + "bidder_request", + "optable-targeting-bidder-request-hook", 250L); final CompositeHookExecutionPlan target = CompositeHookExecutionPlan.of(null); final Account account = givenAccount("accountId", accountPlan); // when and then - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(250L); - assertThat(target.getOptableTargetingProcessedAuctionRequestTimeout(account)).isEqualTo(250L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(250L); + assertThat(target.getOptableTargetingBidderRequestTimeout(account)).isEqualTo(250L); } private ExecutionPlan givenExecutionPlan(String stage, String hookCode) { From 6676f281783e621c1c0d238fcac19dcca2b271c1 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 20:02:36 +0200 Subject: [PATCH 10/19] only block processed-auction hook in legacy mode return early from the hook if (hasRawAuctionRequestHook && hasBidderRequestHook) - the new mode --- ...eTargetingProcessedAuctionRequestHook.java | 13 +++++++----- ...getingProcessedAuctionRequestHookTest.java | 21 +++++++++---------- 2 files changed, 18 insertions(+), 16 deletions(-) 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 c3b5da2f4ad..c01496e637d 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 @@ -66,6 +66,11 @@ public Future> call(AuctionRequestPayloa final Account account = invocationContext.auctionContext().getAccount(); final boolean hasRawAuctionRequestHook = hooksExecutionPlan.hasRawAuctionRequestHook(account); final boolean hasBidderRequestHook = hooksExecutionPlan.hasBidderRequestHook(account); + + if (hasRawAuctionRequestHook && hasBidderRequestHook) { + return update(BidRequestCleaner.instance(), moduleContext); + } + final OptableTargetingProperties properties = resolveOptableTargetingProperties(moduleContext, invocationContext); @@ -81,7 +86,7 @@ public Future> call(AuctionRequestPayloa return optableTargetingCall .compose(targetingResult -> { moduleContext.setOptableTargetingExecutionTime(calcAPICallExecutionTime(moduleContext)); - return enrichPayload(hasBidderRequestHook, targetingResult, moduleContext, properties); + return enrichPayload(targetingResult, moduleContext, properties); }) .recover(throwable -> { moduleContext.failWithExecutionTime(calcAPICallExecutionTime(moduleContext)); @@ -90,7 +95,6 @@ public Future> call(AuctionRequestPayloa } private Future> enrichPayload( - boolean perBidderEnrichmentEnabled, TargetingResult targetingResult, ModuleContext moduleContext, OptableTargetingProperties properties) { @@ -98,9 +102,8 @@ private Future> enrichPayload( moduleContext.setTargeting(targetingResult.getAudience()); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - final PayloadUpdate payloadUpdate = perBidderEnrichmentEnabled - ? BidRequestCleaner.instance() - : BidRequestCleaner.instance().andThen(BidRequestEnricher.of(targetingResult, properties))::apply; + final PayloadUpdate payloadUpdate = + BidRequestCleaner.instance().andThen(BidRequestEnricher.of(targetingResult, properties))::apply; return update(payloadUpdate, moduleContext); } 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 1b8b8f9cbd5..ab63e820cd0 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 @@ -234,7 +234,7 @@ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { } @Test - void callShouldReturnResultWithoutEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() { + void callShouldReturnResultWithEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() { // given target = new OptableTargetingProcessedAuctionRequestHook( configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, true)), 0.01); @@ -259,23 +259,22 @@ void callShouldReturnResultWithoutEnrichedBidRequestWhenOnlyBidderRequestHookIsP .payloadUpdate() .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) .bidRequest(); - assertThat(bidRequest.getUser().getEids()).isNull(); - assertThat(bidRequest.getUser().getData()).isNull(); + assertThat(bidRequest.getUser().getEids()) + .flatExtracting(Eid::getUids) + .extracting(Uid::getId) + .containsExactly("id"); + assertThat(bidRequest.getUser().getData()) + .flatExtracting(Data::getSegment) + .extracting(Segment::getId) + .containsExactly("id"); } @Test void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { // given - final ModuleContext moduleContext = new ModuleContext(); target = new OptableTargetingProcessedAuctionRequestHook( configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, true)), 0.01); - when(optableTargeting.getTargeting(any(), any(), any(), any())) - .thenReturn(Future.succeededFuture(givenTargetingResult())); - when(invocationContext.moduleContext()).thenReturn(moduleContext); - when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); - moduleContext.setOptableTargetingCall( - networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( - "key", "tenant", "origin", false), null)); + when(invocationContext.moduleContext()).thenReturn(new ModuleContext()); // when final Future> future = target.call(auctionRequestPayload, From bd69c4bbc26d14306b3f83243fcfb44da3d5a279 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 21:30:03 +0200 Subject: [PATCH 11/19] update sample request body --- .../sample-requests/data.json | 322 +++++++++++++----- ...=> prebid-config-with-optable-legacy.yaml} | 2 +- .../configs/prebid-config-with-optable.yaml | 2 +- ...> sample-app-settings-optable-legacy.yaml} | 34 -- .../configs/sample-app-settings-optable.yaml | 32 +- 5 files changed, 261 insertions(+), 131 deletions(-) rename sample/configs/{prebid-config-with-optable_v2.yaml => prebid-config-with-optable-legacy.yaml} (98%) rename sample/configs/{sample-app-settings-optable_v2.yaml => sample-app-settings-optable-legacy.yaml} (59%) diff --git a/extra/modules/optable-targeting/sample-requests/data.json b/extra/modules/optable-targeting/sample-requests/data.json index d05f9a5eebc..f15c569d11e 100644 --- a/extra/modules/optable-targeting/sample-requests/data.json +++ b/extra/modules/optable-targeting/sample-requests/data.json @@ -1,127 +1,269 @@ { - "test": 1, - "id": "1", - "imp": - [ + "imp": [ { - "id": "1", - "banner": - { - "w": 300, - "h": 250 + "ext": { + "prebid": { + "bidder": { + "triplelift": { + "inventoryCode": "123" + }, + "pubmatic": { + "publisherId": "156209", + "adSlot": "/1234567/test" + }, + "yieldmo": { + "placementId": "12314235" + }, + "openx": { + "delDomain": "domain.openx.net", + "unit": "12324324", + "customParams": { + "sens": [ + "alc" + ] + } + }, + "grid": { + "uid": 123 + }, + "unruly": { + "siteId": 567890 + }, + "conversant": { + "tag_id": "some_id", + "site_id": "345678" + }, + "33across": { + "siteId": "some_id", + "productId": "ddff" + }, + "adform": { + "mname": "some_name", + "mid": 123456789, + "adxDomain": "adx2.adform.net" + }, + "improvedigital": { + "publisherId": 1234, + "placementId": 123456789 + }, + "colossus": { + "groupId": "123" + } + }, + "adunitcode": "AdUnit_Name_1" + } }, - "ext": - { - "prebid": - { - "storedauctionresponse": { "id": "optable-stored-response" }, - "bidder": + "id": "imp_id", + "banner": { + "topframe": 1, + "format": [ { - "appnexus": - { - "placementId": 0 - } + "w": 320, + "h": 50 } - } - } + ] + }, + "bidfloor": 1.0, + "bidfloorcur": "USD" } ], - "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": "5837d278eabede28e37b5766399ed0d1a4cdc36acee8d35710a255032f45beda" + "cur": [ + "USD" + ], + "at": 1, + "device": { + "w": 591, + "h": 771, + "dnt": 0, + "ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "language": "en", + "js": 1, + "ip": "82.212.42.32", + "sua": { + "source": 2, + "platform": { + "brand": "macOS", + "version": [ + "14", + "0", + "0" + ] }, - "eids": - [ + "browsers": [ { - "source": "growthcode.io", - "uids": - [ - { - "id": "fb58593e-7ac6-48bd-b2de-89a758726362", - "atype": 1 - } - ] - }, - { - "source": "pubcid.org", - "uids": [ - { - "id": "test", - "atype": 1 - } + "brand": "Chromium", + "version": [ + "120", + "0", + "0", + "0" ] }, { - "source": "crwdcntrl.net", - "uids": - [ - { - "id": "dd1b31e65f5e45548c11a0275ba3a8072c00e3a2a0493e8f5a8f54f8067e8b00", - "atype": 1 - } + "brand": "Not-A.Brand", + "version": [ + "24", + "0", + "0", + "0" ] }, { - "source": "amxdt.net", - "uids": - [ - { - "id": "amx*3*a583802a-e6fe-48d7-87c6-7db1b6a4a73a*70f06cdcf8ab0b4ac07a56860ed0e0b6ef0388dc0b0ab5a1dd725999d3b339cf", - "atype": 1 - } + "brand": "Google Chrome", + "version": [ + "120", + "0", + "0", + "0" ] - }, + } + ], + "mobile": 0, + "model": "", + "architecture": "arm" + } + }, + "site": { + "domain": "example-site.com", + "publisher": { + "domain": "example-publisher.com", + "id": "1" + }, + "page": "https://example-site.com/sample-article-slug/", + "content": { + "language": "en" + }, + "cat": [ + "IAB8" + ], + "pagecat": [ + "IAB8" + ], + "privacypolicy": 1, + "mobile": 1 + }, + "user": { + "data": [], + "ext": { + "optable": { + "email": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" + }, + "eids": [ { - "source": "audigent.com", - "uids": - [ + "source": "id5-sync.com", + "uids": [ { - "id": "f84456cd3c72296d7898f62e1c46dd964206ff4d47e64b690c3c5a1d6b1bd286", - "atype": 1 + "id": "ID5*a7fF5d6f1GZgAUunYu5C3Edu5H4_nlp9aN34PiHotjzCtKWRh-vXq84ijeusCYHD", + "atype": 1, + "ext": { + "linkType": 2, + "pba": "EGg0hDGKtMSoVfdiLRmUPZc+x7Hm8I37s1sAPcxYmWA=" + } } ] }, { - "source": "adnxs.com", - "uids": - [ + "source": "pubmatic.com", + "uids": [ { - "id": "d4fd63f0f4f7ce0d128348cb145c7e0f" + "id": "A672B525-C053-45D6-B68E-B1DF0B4D6F3B", + "atype": 3, + "ext": { + "provider": "liveintent.com" + } } ] } - ] + ], + "consent": "", + "ConsentedProvidersSettings": { + "consented_providers": "2~70.1516.89.320.2778.2322.510.1516.2343~dv." + } + } + }, + "regs": { + "gpp": "DBABzw~1YNY~BVQqAAAAAgA", + "gpp_sid": [ + 6, + 5, + 7 + ], + "ext": { + "us_privacy": "1YNY" } }, "ext": { "prebid": { "targeting": { - "includebidderkeys": true + "includewinners": true, + "includebidderkeys": false }, - "analytics": - { + "analytics": { "options": { "enableclientdetails": true } - } + }, + "eidPermissions": [ + { + "source": "bidswitch.net", + "bidders": [ + "conversant", + "grid", + "improvedigital", + "pubmatic", + "undertone", + "unruly", + "yieldmo", + "adform", + "colossus", + "openx", + "triplelift" + ] + } + ], + "bidderconfig": [ + { + "bidders": [ + "openx" + ], + "config": { + "ortb2": { + "site": { + "ext": { + "data": { + "refresh": "0", + "vp": "0", + "hvp": "70", + "site_code": [ + "AFOI_2020", + "APIO_2022", + "BIPOC_2024", + "FMLO_2021", + "FMOOI_2022", + "MINO_2021", + "MOMS_2020", + "MRLOI_22", + "NWPR_2021", + "RMOI_24", + "STKCONT_24" + ] + } + } + } + } + } + }, + { + "bidders": [ + "pubmatic" + ], + "config": { + } + } + ] } - } + }, + "tmax": 1000, + "id": "id", + "test": 1 } diff --git a/sample/configs/prebid-config-with-optable_v2.yaml b/sample/configs/prebid-config-with-optable-legacy.yaml similarity index 98% rename from sample/configs/prebid-config-with-optable_v2.yaml rename to sample/configs/prebid-config-with-optable-legacy.yaml index 1d30adf7992..3f7e9b4f5d3 100644 --- a/sample/configs/prebid-config-with-optable_v2.yaml +++ b/sample/configs/prebid-config-with-optable-legacy.yaml @@ -27,7 +27,7 @@ settings: enforce-valid-account: false generate-storedrequest-bidrequest-id: true filesystem: - settings-filename: sample/configs/sample-app-settings-optable_v2.yaml + settings-filename: sample/configs/sample-app-settings-optable-legacy.yaml stored-requests-dir: sample stored-imps-dir: sample stored-responses-dir: sample/stored diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index d9e736e96ac..874c04ea863 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={{TENANT}}&o={{ORIGIN}} + api-endpoint: https://ca.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}} diff --git a/sample/configs/sample-app-settings-optable_v2.yaml b/sample/configs/sample-app-settings-optable-legacy.yaml similarity index 59% rename from sample/configs/sample-app-settings-optable_v2.yaml rename to sample/configs/sample-app-settings-optable-legacy.yaml index 811d129eaf4..7a533da3697 100644 --- a/sample/configs/sample-app-settings-optable_v2.yaml +++ b/sample/configs/sample-app-settings-optable-legacy.yaml @@ -21,14 +21,6 @@ accounts: api-key: key tenant: optable origin: web-sdk-demo - enrichment-percentage: 100 - bidder-enrichment-percentages: - appnexus: 75 - rubicon: 75 - pubmatic: 100 - criteo: 0 - enrich-web: true - enrich-app: true ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: @@ -39,32 +31,6 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { - "raw-auction-request": { - "groups": [ - { - "timeout": 50, - "hook-sequence": [ - { - "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-raw-auction-request-hook" - } - ] - } - ] - }, - "bidder-request": { - "groups": [ - { - "timeout": 50, - "hook-sequence": [ - { - "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-bidder-request-hook" - } - ] - } - ] - }, "processed-auction-request": { "groups": [ { diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 7a533da3697..8d7293e4ff2 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -19,8 +19,17 @@ accounts: modules: optable-targeting: api-key: key - tenant: optable - origin: web-sdk-demo + tenant: prebidtest + origin: js-sdk + enrichment-percentage: 100 + bidder-enrichment-percentages: + appnexus: 75 + rubicon: 75 + pubmatic: 100 + criteo: 0 + enrich-web: true + enrich-app: true + id-prefix-order: "e,v,c" ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: @@ -31,14 +40,27 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { - "processed-auction-request": { + "raw-auction-request": { "groups": [ { - "timeout": 600, + "timeout": 50, "hook-sequence": [ { "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-processed-auction-request-hook" + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, + "bidder-request": { + "groups": [ + { + "timeout": 50, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-bidder-request-hook" } ] } From 43c10adfe172534e3cdcd806c7456da1231267b2 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 21:30:08 +0200 Subject: [PATCH 12/19] update doc --- extra/modules/optable-targeting/README.md | 109 +++++++++++++++++----- 1 file changed, 87 insertions(+), 22 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 3ae7bd5f659..6ab334cb7df 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -12,10 +12,11 @@ Targeting API endpoint is configurable per publisher. ### Execution Plan -This module runs at two stages: +This module runs at three stages: -* Processed Auction Request: to enrich `user.eids` and `user.data`. -* Auction Response: to inject ad server targeting. +* Raw Auction Request: initiates a non-blocking Optable API call early in the auction lifecycle. +* Bidder Request: awaits the API response and enriches individual bidder requests with `user.eids` and `user.data`. +* Auction Response: injects 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. @@ -26,10 +27,8 @@ 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: +To start using the module in PBS-Java you have to enable it and add the hooks into the execution plan in your config +file. Here's the recommended configuration: ```yaml hooks: @@ -40,14 +39,27 @@ hooks: "endpoints": { "/openrtb2/auction": { "stages": { - "processed-auction-request": { + "raw-auction-request": { "groups": [ { - "timeout": 100, + "timeout": 50, "hook-sequence": [ { "module-code": "optable-targeting", - "hook-impl-code": "optable-targeting-processed-auction-request-hook" + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, + "bidder-request": { + "groups": [ + { + "timeout": 50, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-bidder-request-hook" } ] } @@ -83,6 +95,15 @@ Sample module enablement configuration in JSON and YAML formats: "api-endpoint": "endpoint", "api-key": "key", "timeout": 50, + "enrichment-percentage": 100, + "bidder-enrichment-percentages": { + "appnexus": 75, + "rubicon": 75, + "pubmatic": 100, + "criteo": 0 + }, + "enrich-web": true, + "enrich-app": true, "ppid-mapping": { "pubcid.org": "c" }, @@ -98,24 +119,64 @@ Sample module enablement configuration in JSON and YAML formats: api-endpoint: endpoint api-key: key timeout: 50 + enrichment-percentage: 100 + bidder-enrichment-percentages: + appnexus: 75 + rubicon: 75 + pubmatic: 100 + criteo: 0 + enrich-web: true + enrich-app: true ppid-mapping: { "pubcid.org": "c" } adserver-targeting: false ``` +### Migrating from legacy configuration + +Previous versions of the module used a `processed-auction-request` hook (alongside the `auction-response` hook) that both +made the API call and enriched the request synchronously in one step, blocking the auction pipeline. The new +configuration replaces it with two hooks: `raw-auction-request` (initiates the API call early) and `bidder-request` +(awaits the result and enriches per-bidder), while the `auction-response` hook remains unchanged. If your execution plan contains the following fragment, it should +be replaced with the `raw-auction-request` and `bidder-request` hooks shown above: + +```json +"processed-auction-request": { + "groups": [ + { + "timeout": 600, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] +} +``` + +The `processed-auction-request` hook is still supported for backwards compatibility. It detects whether the new hooks +(`raw-auction-request` and `bidder-request`) are present in the execution plan. If both are active, it passes through +immediately without blocking the pipeline. If the new hooks are absent, it falls back to the legacy synchronous +behavior. This means the legacy fragment can be kept during migration without negating the latency benefit of the new +configuration. + ### 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. +The timeout value specified in the execution plan for the `bidder-request` hook should allow enough time for the Optable +Targeting API call to complete. The API call is initiated earlier in the `raw-auction-request` stage and runs in parallel +with other auction processing, so the effective wait time at the `bidder-request` stage is typically much shorter than +the full API roundtrip. **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. +no HTTP calls made in this hook - Optable-specific keywords are cached on earlier stages and retrieved from the module +invocation context later. ## Module Configuration Parameters for PBS-Java @@ -133,14 +194,18 @@ would result in this nesting in the JSON configuration: ``` -| 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 | +| 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 | none | A soft timeout (in ms) sent as a hint to the Targeting API endpoint to limit the request times to Optable's external tokenizer services | +| 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 | +| enrichment-percentage | no | integer | 100 | Default percentage (0-100) of bid requests per bidder that will receive enrichment data. Set to 100 to enrich all requests, 0 to disable enrichment by default. | +| bidder-enrichment-percentages | no | map | none | Per-bidder overrides for `enrichment-percentage`. Keys are bidder names, values are percentages (0-100). F.e. `{"appnexus": 75, "criteo": 0}` enriches 75% of appnexus requests and none for criteo. | +| enrich-web | no | boolean | true | Whether to enrich web traffic (requests with a `site` object). | +| enrich-app | no | boolean | true | Whether to enrich app traffic (requests with an `app` object). | ## ID Mapping From 71bdbb0ddfde76b000afaaf5ab9f5abf3bb6dcaa Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 21:30:33 +0200 Subject: [PATCH 13/19] BidRequestCleaner in the raw-auction-request --- .../targeting/v1/OptableRawAuctionRequestHook.java | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java index 1c53f7ea4e9..325bafc5cf0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -97,17 +97,7 @@ public Future> call(AuctionRequestPayloa moduleContext.setOptableTargetingCall(optableTargetingCall); - return updateModuleContext(moduleContext); - } - - private static Future> updateModuleContext(ModuleContext moduleContext) { - - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .moduleContext(moduleContext) - .build()); + return update(BidRequestCleaner.instance(), moduleContext); } public static Future> update( From 4eea424d44a8324b355fca1b55fd4cb8ca831772 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 23:38:22 +0200 Subject: [PATCH 14/19] increase bidder-request timeout in the sample config --- sample/configs/sample-app-settings-optable.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 8d7293e4ff2..8641c055708 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -56,7 +56,7 @@ accounts: "bidder-request": { "groups": [ { - "timeout": 50, + "timeout": 500, "hook-sequence": [ { "module-code": "optable-targeting", From 3c633930202edb637b87eed6a9a9cbda372e417b Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 23:40:09 +0200 Subject: [PATCH 15/19] gpp_sid serialization bug fix --- .../targeting/v1/core/QueryBuilder.java | 3 +- .../targeting/v1/core/QueryBuilderTest.java | 74 +++++++++++++++++++ 2 files changed, 76 insertions(+), 1 deletion(-) 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 613286f4b92..bee89818b2f 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 @@ -78,7 +78,8 @@ private static String buildAttributesString(OptableAttributes optableAttributes) .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())); + .ifPresent(gppSids -> sb.append("&gpp_sid=").append( + gppSids.stream().limit(2).map(String::valueOf).collect(Collectors.joining(",")))); Optional.ofNullable(optableAttributes.getTimeout()) .ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms")); 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 0359578ee75..68d0c5b099a 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 @@ -6,6 +6,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Query; import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; @@ -112,6 +113,79 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { assertThat(query).isNull(); } + @Test + public void shouldBuildQueryStringWithGppSid() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email")); + final OptableAttributes attributes = OptableAttributes.builder() + .ips(List.of("8.8.8.8")) + .gpp("DBABzw~1YNY~BVQqAAAAAgA") + .gppSid(Set.of(7, 22)) + .build(); + + // when + final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + + // then + assertThat(query).contains("&gpp=DBABzw~1YNY~BVQqAAAAAgA"); + assertThat(query).containsPattern("&gpp_sid=\\d+,\\d+"); + assertThat(query).doesNotContain("Optional"); + } + + @Test + public void shouldBuildQueryStringWithSingleGppSid() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email")); + final OptableAttributes attributes = OptableAttributes.builder() + .ips(List.of("8.8.8.8")) + .gpp("DBABzw~1YNY") + .gppSid(Set.of(7)) + .build(); + + // when + final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + + // then + assertThat(query).contains("&gpp_sid=7"); + assertThat(query).doesNotContain("Optional"); + } + + @Test + public void shouldLimitGppSidToTwoValues() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email")); + final OptableAttributes attributes = OptableAttributes.builder() + .ips(List.of("8.8.8.8")) + .gpp("DBABzw~1YNY~BVQqAAAAAgA") + .gppSid(Set.of(5, 7, 22)) + .build(); + + // when + final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + + // then + final String gppSidValue = query.split("gpp_sid=")[1].split("&")[0]; + assertThat(gppSidValue.split(",")).hasSize(2); + assertThat(query).doesNotContain("Optional"); + } + + @Test + public void shouldNotIncludeGppSidWhenEmpty() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email")); + final OptableAttributes attributes = OptableAttributes.builder() + .ips(List.of("8.8.8.8")) + .gpp("DBABzw~1YNY") + .gppSid(Set.of()) + .build(); + + // when + final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + + // then + assertThat(query).doesNotContain("gpp_sid"); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) From 5424150737f26576aeb14ad0934e94fec9890603 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 23:42:18 +0200 Subject: [PATCH 16/19] update readme re: timeout --- extra/modules/optable-targeting/README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 6ab334cb7df..a4bdc8c8088 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -165,10 +165,11 @@ configuration. ### Timeout considerations -The timeout value specified in the execution plan for the `bidder-request` hook should allow enough time for the Optable -Targeting API call to complete. The API call is initiated earlier in the `raw-auction-request` stage and runs in parallel -with other auction processing, so the effective wait time at the `bidder-request` stage is typically much shorter than -the full API roundtrip. +The `bidder-request` hook timeout is used as the timeout budget for the Optable Targeting API call Future that is +initiated in the `raw-auction-request` stage. The API call runs in parallel with other auction processing, so the +effective wait time at the `bidder-request` stage is typically much shorter than the full API roundtrip. The +`raw-auction-request` hook timeout only needs to cover its own lightweight setup (validation, sampling) and can be kept +short. **Note:** Do not confuse hook timeout value with the module timeout parameter which is optional. The hook timeout value would depend on the cloud/region where the PBS instance is hosted and the latency to reach the Optable's servers. This From 87ae03e54d0efd0c3f7420d46e47f0b18969292f Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 26 May 2026 00:00:22 +0200 Subject: [PATCH 17/19] sampler bug fixed, it was always enrichin for 0 random is 0-99, percentage is 0-100. enrich when random < (strictly less than) percentage. so if we specify percentage 0 we never enrich, if we specify percentage 100 we always enrich. --- .../v1/OptableBidderRequestHook.java | 3 ++- .../v1/core/BidderEnrichmentSampler.java | 2 +- .../v1/OptableBidderRequestHookTest.java | 3 +++ .../v1/core/BidderEnrichmentSamplerTest.java | 24 +++---------------- .../configs/sample-app-settings-optable.yaml | 4 ++-- 5 files changed, 11 insertions(+), 25 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index 467124c1286..0e6a030b99d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java @@ -33,7 +33,8 @@ public Future> call(BidderRequestPayload } final Set biddersToEnrich = moduleContext.getBiddersToEnrich(); - if (CollectionUtils.isEmpty(biddersToEnrich)) { + if (CollectionUtils.isEmpty(biddersToEnrich) + || !biddersToEnrich.contains(invocationContext.bidder())) { return noAction(moduleContext); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java index 95fe7820176..9e714e74a54 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSampler.java @@ -37,7 +37,7 @@ public Set sample(BidRequest bidRequest, OptableTargetingProperties opta .filter(bidder -> { final int percentage = resolvePercentage(aliases, bidder, defaultEnrichmentPercentage, bidderEnrichmentPercentage); - return randomSupplier.getAsInt() <= percentage; + return percentage > 0 && randomSupplier.getAsInt() < percentage; }) .collect(Collectors.toSet()); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java index ac14a7dc3eb..14a9c521611 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -117,6 +117,7 @@ public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() { moduleContext.setBiddersToEnrich(Set.of("bidder1")); moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); // when final Future> future = @@ -148,6 +149,7 @@ public void shouldUpdateModuleContextWithTargetingOnSuccess() { moduleContext.setBiddersToEnrich(Set.of("bidder1")); moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); // when target.call(bidderRequestPayload, invocationContext); @@ -169,6 +171,7 @@ public void shouldReturnNoActionWhenTargetingCallFails() { moduleContext.setOptableTargetingCall( Future.failedFuture(new RuntimeException("targeting service error"))); when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); // when final Future> future = diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java index bfcc3057b76..914079178e7 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidderEnrichmentSamplerTest.java @@ -116,7 +116,6 @@ public void sampleShouldIncludeAllBiddersWhenDefaultPercentageIs100() { public void sampleShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); - given(randomSupplier.getAsInt()).willReturn(0); final BidRequest bidRequest = givenBidRequest( request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA", "bidderB")))))); @@ -129,7 +128,7 @@ public void sampleShouldExcludeAllBiddersWhenDefaultPercentageIsNegative() { } @Test - public void sampleShouldIncludeBidderWhenRandomValueEqualsPercentage() { + public void sampleShouldExcludeBidderWhenRandomValueEqualsPercentage() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); given(randomSupplier.getAsInt()).willReturn(50); @@ -141,7 +140,7 @@ public void sampleShouldIncludeBidderWhenRandomValueEqualsPercentage() { final Set result = target.sample(bidRequest, givenSampleProperties(50, Collections.emptyMap())); // then - assertThat(result).containsExactly("bidderA"); + assertThat(result).isEmpty(); } @Test @@ -177,26 +176,9 @@ public void sampleShouldExcludeBidderWhenRandomValueExceedsPercentage() { } @Test - public void sampleShouldIncludeBidderWhenPercentageIsZeroAndRandomIsZero() { - // given - given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); - given(randomSupplier.getAsInt()).willReturn(0); - - final BidRequest bidRequest = givenBidRequest( - request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); - - // when - final Set result = target.sample(bidRequest, givenSampleProperties(0, Collections.emptyMap())); - - // then - assertThat(result).containsExactly("bidderA"); - } - - @Test - public void sampleShouldExcludeBidderWhenPercentageIsZeroAndRandomIsOne() { + public void sampleShouldExcludeBidderWhenPercentageIsZeroAndRandomIsZero() { // given given(bidderAliases.resolveBidder(any())).willAnswer(inv -> inv.getArgument(0)); - given(randomSupplier.getAsInt()).willReturn(1); final BidRequest bidRequest = givenBidRequest( request -> request.imp(List.of(givenImp(imp -> imp.ext(givenBidderExt("bidderA")))))); diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 8641c055708..4413a05769d 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -25,8 +25,8 @@ accounts: bidder-enrichment-percentages: appnexus: 75 rubicon: 75 - pubmatic: 100 - criteo: 0 + pubmatic: 0 + improvedigital: 50 enrich-web: true enrich-app: true id-prefix-order: "e,v,c" From 3151f5b5a1340852612c39067837dd1f4c5b912e Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 25 May 2026 22:37:07 +0200 Subject: [PATCH 18/19] optable-targeting: add analytic tags --- .../v1/OptableBidderRequestHook.java | 31 +++++++++++++++---- .../v1/core/AnalyticTagsResolver.java | 15 +++++++++ .../v1/OptableBidderRequestHookTest.java | 11 +++++++ .../v1/core/BidRequestCleanerTest.java | 25 +++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index 0e6a030b99d..d8bcdfea72d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java @@ -7,6 +7,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidderRequestEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -41,18 +42,22 @@ public Future> call(BidderRequestPayload return moduleContext.getOptableTargetingCall() .compose(targetingResult -> enrichedPayload( - targetingResult, moduleContext, moduleContext.getOptableTargetingProperties())) - .recover(throwable -> noAction(moduleContext)); + targetingResult, + moduleContext, + moduleContext.getOptableTargetingProperties(), + invocationContext.bidder())) + .recover(throwable -> error(moduleContext, invocationContext.bidder())); } private Future> enrichedPayload(TargetingResult targetingResult, - ModuleContext moduleContext, - OptableTargetingProperties properties) { + ModuleContext moduleContext, + OptableTargetingProperties properties, + String bidderName) { moduleContext.setTargeting(targetingResult.getAudience()); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); - return update(BidderRequestEnricher.of(targetingResult, properties), moduleContext); + return update(BidderRequestEnricher.of(targetingResult, properties), moduleContext, bidderName); } private Future> noAction(ModuleContext moduleContext) { @@ -64,15 +69,29 @@ private Future> noAction(ModuleContext mo .build()); } + private Future> error(ModuleContext moduleContext, String bidderName) { + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags( + AnalyticTagsResolver.toEnrichErrorBidderRequestAnalyticTags(bidderName)) + .moduleContext(moduleContext) + .build()); + } + private static Future> update( PayloadUpdate payloadUpdate, - ModuleContext moduleContext) { + ModuleContext moduleContext, + String bidderName) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) .payloadUpdate(payloadUpdate) + .analyticsTags( + AnalyticTagsResolver.toEnrichBidderRequestAnalyticTags(bidderName, moduleContext)) .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 index 80905ce088c..e76f213f26d 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 @@ -24,6 +24,7 @@ public class AnalyticTagsResolver { 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 static final String STATUS_BIDDER_NAME = "bidder"; private AnalyticTagsResolver() { } @@ -35,6 +36,20 @@ public static Tags toEnrichRequestAnalyticTags(ModuleContext moduleContext) { toResults(STATUS_EXECUTION_TIME, String.valueOf(moduleContext.getOptableTargetingExecutionTime()))))); } + public static Tags toEnrichBidderRequestAnalyticTags(String bidderName, ModuleContext moduleContext) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY_ENRICH_REQUEST, + toEnrichmentStatusValue(moduleContext.getEnrichRequestStatus()), + toResults(STATUS_BIDDER_NAME, bidderName)))); + } + + public static Tags toEnrichErrorBidderRequestAnalyticTags(String bidderName) { + return TagsImpl.of(Collections.singletonList(ActivityImpl.of( + ACTIVITY_ENRICH_REQUEST, + toEnrichmentStatusValue(EnrichmentStatus.failure()), + toResults(STATUS_BIDDER_NAME, bidderName)))); + } + public static Tags toEnrichResponseAnalyticTags(ModuleContext moduleContext) { final List activities = new ArrayList<>(); if (moduleContext.isAdserverTargetingEnabled()) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java index 14a9c521611..9f4ea2c3071 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -42,6 +42,7 @@ public class OptableBidderRequestHookTest extends BaseOptableTest { public void setUp() { target = new OptableBidderRequestHook(); when(bidderRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(invocationContext.bidder()).thenReturn("bidder1"); } @Test @@ -139,6 +140,11 @@ public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() { .isEqualTo("id"); assertThat(enrichedRequest.getUser().getData().getFirst().getSegment().getFirst().getId()) .isEqualTo("id"); + assertThat(result.analyticsTags().activities().getFirst()) + .satisfies(activity -> { + assertThat(activity.status()).isEqualTo("success"); + assertThat(activity.results().getFirst().values().get("bidder").asText()).isEqualTo("bidder1"); + }); } @Test @@ -183,6 +189,11 @@ public void shouldReturnNoActionWhenTargetingCallFails() { assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(result.analyticsTags().activities().getFirst()) + .satisfies(activity -> { + assertThat(activity.status()).isEqualTo("fail"); + assertThat(activity.results().getFirst().values().get("bidder").asText()).isEqualTo("bidder1"); + }); } private static ModuleContext givenModuleContextWithProperties(OptableTargetingProperties properties) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java index b6d72748fbd..6aa4ebff3a8 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java @@ -27,4 +27,29 @@ public void shouldRemoveUserExtOptableTag() { .extracting(it -> it.getProperty("optable")) .isEqualTo(null); } + + @Test + public void shouldKeepOtherUserExtOptableTags() { + // given + final User user = givenUser(); + ((com.fasterxml.jackson.databind.node.ObjectNode) user.getExt().getProperty("optable")) + .put("other", "value"); + + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> + bidRequest.user(user))); + + // when + final AuctionRequestPayload result = BidRequestCleaner.instance().apply(auctionRequestPayload); + + // then + assertThat(result).extracting(AuctionRequestPayload::bidRequest) + .extracting(BidRequest::getUser) + .extracting(User::getExt) + .extracting(it -> (com.fasterxml.jackson.databind.node.ObjectNode) it.getProperty("optable")) + .isNotNull() + .satisfies(optable -> { + assertThat(optable.has("other")).isTrue(); + assertThat(optable.has("email")).isFalse(); + }); + } } From 49f0c67be68606676b4275cc9cde65079c2ec9bd Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 26 May 2026 08:20:34 +0200 Subject: [PATCH 19/19] set sample email hash that leads to enrichment --- extra/modules/optable-targeting/sample-requests/data.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/sample-requests/data.json b/extra/modules/optable-targeting/sample-requests/data.json index f15c569d11e..b59400cc6d4 100644 --- a/extra/modules/optable-targeting/sample-requests/data.json +++ b/extra/modules/optable-targeting/sample-requests/data.json @@ -146,7 +146,7 @@ "data": [], "ext": { "optable": { - "email": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2" + "email": "5837d278eabede28e37b5766399ed0d1a4cdc36acee8d35710a255032f45beda" }, "eids": [ {