From 11b1195026e4fcda69646ace594810b3dc0f24b6 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 4 May 2026 19:13:16 +0200 Subject: [PATCH 01/30] 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 66ed6f1d1ca..644829188ed 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 a3ab28d098f..394bf7da88a 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 5e2d1b8d002e62b2a04828836c8c7abfec9ea14c Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 11 May 2026 21:22:08 +0200 Subject: [PATCH 02/30] 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 5e26ce7c23704e79b4c44dc0eb4d768b0451533e Mon Sep 17 00:00:00 2001 From: softcoder Date: Wed, 13 May 2026 20:43:12 +0200 Subject: [PATCH 03/30] 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 617bdf2107d88d736abec932da4cd5f05e51c438 Mon Sep 17 00:00:00 2001 From: softcoder Date: Wed, 13 May 2026 22:11:17 +0200 Subject: [PATCH 04/30] 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 394bf7da88a..04bb5e53d75 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 3d3ea39e97cb446b327df09a278a04ecb313ff0f Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 14 May 2026 00:25:53 +0200 Subject: [PATCH 05/30] 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 8403993d94e04c511eee0faa2828a704ebf1f1a1 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 14 May 2026 19:35:30 +0200 Subject: [PATCH 06/30] 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 04bb5e53d75..52143ff9fef 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 dc8cfd061ff0fb36a12bedc8065755049ebe2c10 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 14 May 2026 21:20:52 +0200 Subject: [PATCH 07/30] 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 b38c68bc4a33fd7d1623f1abd408eaf4384512a7 Mon Sep 17 00:00:00 2001 From: softcoder Date: Sat, 23 May 2026 21:32:37 +0200 Subject: [PATCH 08/30] 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 6ca0d9cddd24b8485c52539632672fb63acd0e6b Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 20:01:20 +0200 Subject: [PATCH 09/30] 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 abe07ddbf9ff47eebc3684f7464efc1fcb961621 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 20:02:36 +0200 Subject: [PATCH 10/30] 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 c8842c1d7047d35f33dd989d407deb2e41ef25eb Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 21:30:03 +0200 Subject: [PATCH 11/30] 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 d5a938f47ac..17ec6a77be9 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -52,4 +52,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 6aa7fb964b1ecfaa7fc4679510c4b0340cc21ea3 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 21:30:08 +0200 Subject: [PATCH 12/30] 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 87d6d4d2813ef5935547d371ddca9d2a83df72bb Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 21:30:33 +0200 Subject: [PATCH 13/30] 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 b719a3ad7aa44c1fc6e6d1d314fba54907d52500 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 23:38:22 +0200 Subject: [PATCH 14/30] 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 7ca5987689d10e4c0a11f9829a737f5f220f845d Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 23:40:09 +0200 Subject: [PATCH 15/30] 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 6580a76524e23c57f71efa1e9fce484c383aa5df Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Mon, 25 May 2026 23:42:18 +0200 Subject: [PATCH 16/30] 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 a5a960aa4b08ab22afeb4dcd2bb5a19052b4aef3 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 26 May 2026 00:00:22 +0200 Subject: [PATCH 17/30] 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 39da4dd22c20a9c0cc0c441a4c1a38d64d93f0c8 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 25 May 2026 22:37:07 +0200 Subject: [PATCH 18/30] 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 b2474323dc276ecfa30c5acc64bdc2cea253f07b Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 26 May 2026 08:20:34 +0200 Subject: [PATCH 19/30] 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": [ { From becbd2e83fba3cac0efa40099a8645608b7fab1c Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 2 Jun 2026 13:52:06 +0200 Subject: [PATCH 20/30] improve analytics tags --- .../v1/OptableBidderRequestHook.java | 80 +++++++++++------ .../v1/core/AnalyticTagsResolver.java | 30 ++++--- .../v1/OptableBidderRequestHookTest.java | 90 ++++++++++++++++--- 3 files changed, 151 insertions(+), 49 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 d8bcdfea72d..7c544a47126 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 @@ -6,6 +6,7 @@ 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.Ortb2; 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; @@ -13,11 +14,14 @@ import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.analytics.Tags; import org.prebid.server.hooks.v1.bidder.BidderInvocationContext; import org.prebid.server.hooks.v1.bidder.BidderRequestHook; import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; +import java.util.Optional; import java.util.Set; +import java.util.concurrent.TimeoutException; public class OptableBidderRequestHook implements BidderRequestHook { @@ -30,52 +34,75 @@ public Future> call(BidderRequestPayload final ModuleContext moduleContext = ModuleContext.of(invocationContext); final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties(); if (!properties.isPerBidderEnrichmentEnabled()) { - return noAction(moduleContext); + return noAction(moduleContext, null); } final Set biddersToEnrich = moduleContext.getBiddersToEnrich(); if (CollectionUtils.isEmpty(biddersToEnrich) || !biddersToEnrich.contains(invocationContext.bidder())) { - return noAction(moduleContext); + return noAction(moduleContext, null); } + final String bidder = invocationContext.bidder(); + return moduleContext.getOptableTargetingCall() .compose(targetingResult -> - enrichedPayload( - targetingResult, - moduleContext, - moduleContext.getOptableTargetingProperties(), - invocationContext.bidder())) - .recover(throwable -> error(moduleContext, invocationContext.bidder())); + enrichedPayload(targetingResult, moduleContext, properties, bidder)) + .recover(throwable -> failedAction(moduleContext, throwable, bidder)); } private Future> enrichedPayload(TargetingResult targetingResult, - ModuleContext moduleContext, - OptableTargetingProperties properties, - String bidderName) { + ModuleContext moduleContext, + OptableTargetingProperties properties, + String bidder) { + + final long executionTime = calcExecutionTime(moduleContext); + final boolean hasData = hasEnrichmentData(targetingResult); + + if (hasData) { + moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + } - moduleContext.setTargeting(targetingResult.getAudience()); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); + final String outcome = hasData ? "enriched" : "no-data"; + final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags( + bidder, outcome, executionTime); - return update(BidderRequestEnricher.of(targetingResult, properties), moduleContext, bidderName); + return hasData + ? update(BidderRequestEnricher.of(targetingResult, properties), moduleContext, analyticsTags) + : noAction(moduleContext, analyticsTags); } - private Future> noAction(ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .moduleContext(moduleContext) - .build()); + private Future> failedAction(ModuleContext moduleContext, + Throwable throwable, + String bidder) { + + final long executionTime = calcExecutionTime(moduleContext); + final String outcome = throwable instanceof TimeoutException ? "timeout" : "error"; + final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags( + bidder, outcome, executionTime); + + return noAction(moduleContext, analyticsTags); + } + + private static boolean hasEnrichmentData(TargetingResult targetingResult) { + return Optional.ofNullable(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .isPresent(); + } + + private static long calcExecutionTime(ModuleContext moduleContext) { + final long startTime = moduleContext.getCallTargetingAPITimestamp(); + return startTime > 0 ? System.currentTimeMillis() - startTime : 0; } - private Future> error(ModuleContext moduleContext, String bidderName) { + private Future> noAction(ModuleContext moduleContext, Tags analyticsTags) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.no_action) - .analyticsTags( - AnalyticTagsResolver.toEnrichErrorBidderRequestAnalyticTags(bidderName)) + .analyticsTags(analyticsTags) .moduleContext(moduleContext) .build()); } @@ -83,15 +110,14 @@ private Future> error(ModuleContext modul private static Future> update( PayloadUpdate payloadUpdate, ModuleContext moduleContext, - String bidderName) { + Tags analyticsTags) { return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) .action(InvocationAction.update) + .analyticsTags(analyticsTags) .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 e76f213f26d..5504ddfea26 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 @@ -2,6 +2,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode; import org.prebid.server.hooks.execution.v1.analytics.ActivityImpl; +import org.prebid.server.hooks.execution.v1.analytics.AppliedToImpl; import org.prebid.server.hooks.execution.v1.analytics.ResultImpl; import org.prebid.server.hooks.execution.v1.analytics.TagsImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; @@ -21,10 +22,11 @@ public class AnalyticTagsResolver { private static final String ACTIVITY_ENRICH_REQUEST = "optable-enrich-request"; + private static final String ACTIVITY_ENRICH_BIDDER_REQUEST = "optable-enrich-bidder-request"; private static final String ACTIVITY_ENRICH_RESPONSE = "optable-enrich-response"; private static final String STATUS_EXECUTION_TIME = "execution-time"; + private static final String STATUS_OUTCOME = "outcome"; private static final String STATUS_REASON = "reason"; - private static final String STATUS_BIDDER_NAME = "bidder"; private AnalyticTagsResolver() { } @@ -36,18 +38,24 @@ 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 toBidderEnrichRequestAnalyticTags(String bidder, + String outcome, + long executionTime) { + + final ObjectNode values = ObjectMapperProvider.mapper().createObjectNode() + .put(STATUS_OUTCOME, outcome) + .put(STATUS_EXECUTION_TIME, String.valueOf(executionTime)); - public static Tags toEnrichErrorBidderRequestAnalyticTags(String bidderName) { return TagsImpl.of(Collections.singletonList(ActivityImpl.of( - ACTIVITY_ENRICH_REQUEST, - toEnrichmentStatusValue(EnrichmentStatus.failure()), - toResults(STATUS_BIDDER_NAME, bidderName)))); + ACTIVITY_ENRICH_BIDDER_REQUEST, + "enriched".equals(outcome) ? Status.SUCCESS.getValue() : Status.FAIL.getValue(), + Collections.singletonList(ResultImpl.of( + null, + values, + AppliedToImpl.builder() + .bidders(Collections.singletonList(bidder)) + .request(true) + .build()))))); } public static Tags toEnrichResponseAnalyticTags(ModuleContext moduleContext) { 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 9f4ea2c3071..2bcce42ac94 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 @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; @@ -17,11 +18,13 @@ 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.Tags; 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 java.util.concurrent.TimeoutException; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.when; @@ -116,6 +119,7 @@ public void shouldReturnUpdateActionWhenTargetingResultIsAvailable() { final ModuleContext moduleContext = givenModuleContextWithProperties( givenPropertiesWithPerBidderEnrichmentEnabled()); moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100); moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); when(invocationContext.bidder()).thenReturn("bidder1"); @@ -140,11 +144,8 @@ 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"); - }); + + assertAnalyticsTags(result.analyticsTags(), "bidder1", "enriched", "success"); } @Test @@ -153,6 +154,7 @@ public void shouldUpdateModuleContextWithTargetingOnSuccess() { final ModuleContext moduleContext = givenModuleContextWithProperties( givenPropertiesWithPerBidderEnrichmentEnabled()); moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100); moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); when(invocationContext.bidder()).thenReturn("bidder1"); @@ -169,11 +171,38 @@ public void shouldUpdateModuleContextWithTargetingOnSuccess() { } @Test - public void shouldReturnNoActionWhenTargetingCallFails() { + public void shouldReturnNoActionWithNoDataOutcomeWhenTargetingResultHasNoUser() { // given final ModuleContext moduleContext = givenModuleContextWithProperties( givenPropertiesWithPerBidderEnrichmentEnabled()); moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 50); + moduleContext.setOptableTargetingCall(Future.succeededFuture(givenEmptyTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + + assertThat(moduleContext.getTargeting()).isNull(); + assertAnalyticsTags(result.analyticsTags(), "bidder1", "no-data", "fail"); + } + + @Test + public void shouldReturnNoActionWithErrorOutcomeWhenTargetingCallFails() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 50); moduleContext.setOptableTargetingCall( Future.failedFuture(new RuntimeException("targeting service error"))); when(invocationContext.moduleContext()).thenReturn(moduleContext); @@ -189,11 +218,34 @@ 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"); - }); + + assertAnalyticsTags(result.analyticsTags(), "bidder1", "error", "fail"); + } + + @Test + public void shouldReturnNoActionWithTimeoutOutcomeWhenTargetingCallTimesOut() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 500); + moduleContext.setOptableTargetingCall( + Future.failedFuture(new TimeoutException("Timeout has been exceeded"))); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + + assertAnalyticsTags(result.analyticsTags(), "bidder1", "timeout", "fail"); } private static ModuleContext givenModuleContextWithProperties(OptableTargetingProperties properties) { @@ -207,4 +259,20 @@ private OptableTargetingProperties givenPropertiesWithPerBidderEnrichmentEnabled properties.setEnrichmentPercentage(50); return properties; } + + private void assertAnalyticsTags(Tags tags, String expectedBidder, String expectedOutcome, String expectedStatus) { + assertThat(tags).isNotNull(); + assertThat(tags.activities()).hasSize(1); + assertThat(tags.activities().getFirst().name()).isEqualTo("optable-enrich-bidder-request"); + assertThat(tags.activities().getFirst().status()).isEqualTo(expectedStatus); + + assertThat(tags.activities().getFirst().results()).hasSize(1); + final JsonNode values = tags.activities().getFirst().results().getFirst().values(); + assertThat(values.get("outcome").asText()).isEqualTo(expectedOutcome); + assertThat(values.has("execution-time")).isTrue(); + + assertThat(tags.activities().getFirst().results().getFirst().appliedTo()).isNotNull(); + assertThat(tags.activities().getFirst().results().getFirst().appliedTo().bidders()) + .containsExactly(expectedBidder); + } } From 000d38998f2f7d56f89a88c1d5b339c44f923e43 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Tue, 2 Jun 2026 13:57:54 +0200 Subject: [PATCH 21/30] improve doc --- extra/modules/optable-targeting/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index a4bdc8c8088..9e3b76c6446 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -204,7 +204,7 @@ would result in this nesting in the JSON configuration: | 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. | +| 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. Bidders not listed in this map fall back to the default `enrichment-percentage` (100% unless overridden). | | 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). | From a808b497a90729ac8dc38bc727b678a2039a0d77 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 3 Jun 2026 22:17:53 +0200 Subject: [PATCH 22/30] remove legacy config files --- .../prebid-config-with-optable-legacy.yaml | 53 ---------------- .../sample-app-settings-optable-legacy.yaml | 63 ------------------- 2 files changed, 116 deletions(-) delete mode 100644 sample/configs/prebid-config-with-optable-legacy.yaml delete mode 100644 sample/configs/sample-app-settings-optable-legacy.yaml diff --git a/sample/configs/prebid-config-with-optable-legacy.yaml b/sample/configs/prebid-config-with-optable-legacy.yaml deleted file mode 100644 index 3f7e9b4f5d3..00000000000 --- a/sample/configs/prebid-config-with-optable-legacy.yaml +++ /dev/null @@ -1,53 +0,0 @@ -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-legacy.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-legacy.yaml b/sample/configs/sample-app-settings-optable-legacy.yaml deleted file mode 100644 index 7a533da3697..00000000000 --- a/sample/configs/sample-app-settings-optable-legacy.yaml +++ /dev/null @@ -1,63 +0,0 @@ -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" - } - ] - } - ] - } - } - } - } - } From e647f23f2e6b84e1aec4969e4c390e3a331574e7 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 3 Jun 2026 22:31:03 +0200 Subject: [PATCH 23/30] update gpp_sid test --- .../modules/optable/targeting/v1/core/QueryBuilderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 68d0c5b099a..16d5953ef71 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 @@ -128,7 +128,8 @@ public void shouldBuildQueryStringWithGppSid() { // then assertThat(query).contains("&gpp=DBABzw~1YNY~BVQqAAAAAgA"); - assertThat(query).containsPattern("&gpp_sid=\\d+,\\d+"); + final String gppSidValue = query.split("gpp_sid=")[1].split("&")[0]; + assertThat(gppSidValue.split(",")).containsExactlyInAnyOrder("7", "22"); assertThat(query).doesNotContain("Optional"); } From b7bcda83e2755a00a8927d2a63a8e85bac364045 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 3 Jun 2026 22:31:18 +0200 Subject: [PATCH 24/30] shorten fully qualified type name --- .../optable/targeting/v1/core/RequestEnricher.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) 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 index 634fcb000cc..f2b9a169a49 100644 --- 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 @@ -5,12 +5,12 @@ import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Segment; import com.iab.openrtb.request.Uid; +import com.iab.openrtb.request.User; import org.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; @@ -38,7 +38,7 @@ protected BidRequest enrichBidRequest(BidRequest bidRequest) { return bidRequest; } - final User optableUser = Optional.of(targetingResult) + final var optableUser = Optional.of(targetingResult) .map(TargetingResult::getOrtb2) .map(Ortb2::getUser) .orElse(null); @@ -47,15 +47,18 @@ protected BidRequest enrichBidRequest(BidRequest bidRequest) { return bidRequest; } - final com.iab.openrtb.request.User bidRequestUser = Optional.ofNullable(bidRequest.getUser()) - .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); + final User bidRequestUser = Optional.ofNullable(bidRequest.getUser()) + .orElseGet(() -> 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) { + private User mergeUserData( + User user, + org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User optableUser) { + return user.toBuilder() .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids()))) .data(mergeData(user.getData(), optableUser.getData())) From 7df477e7d6d8b615f0ac5cebaa186a9a996d8a0f Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Thu, 4 Jun 2026 00:28:48 +0200 Subject: [PATCH 25/30] rename NetworkCall -> TargetingRequestExecutor --- .../config/OptableTargetingConfig.java | 12 +++--- .../v1/OptableRawAuctionRequestHook.java | 10 ++--- ...eTargetingProcessedAuctionRequestHook.java | 10 ++--- ...all.java => TargetingRequestExecutor.java} | 4 +- .../v1/OptableRawAuctionRequestHookTest.java | 12 +++--- ...getingProcessedAuctionRequestHookTest.java | 39 +++++++++++++------ 6 files changed, 51 insertions(+), 36 deletions(-) rename extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/{NetworkCall.java => TargetingRequestExecutor.java} (97%) 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 ceeb947c3ec..cc05cfe2a02 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 @@ -19,7 +19,7 @@ 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; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; @@ -97,16 +97,16 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties } @Bean - NetworkCall networkCall(OptableTargeting optableTargeting, + TargetingRequestExecutor targetingRequestExecutor(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask, TimeoutFactory timeoutFactory) { - return new NetworkCall(optableTargeting, userFpdActivityMask, timeoutFactory); + return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); } @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, - NetworkCall networkCall, + TargetingRequestExecutor targetingRequestExecutor, JsonMerger jsonMerger, BidderCatalog bidderCatalog, JacksonMapper mapper, @@ -122,13 +122,13 @@ OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, return new OptableTargetingModule(List.of( new OptableRawAuctionRequestHook( configResolver, - networkCall, + targetingRequestExecutor, BidderEnrichmentSampler.of(AliasesResolver.of(bidderCatalog)), hooksExecutionPlan, logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, - networkCall, + targetingRequestExecutor, hooksExecutionPlan, logSamplingRate), new OptableBidderRequestHook(), 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 325bafc5cf0..79102d98bbb 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 @@ -11,7 +11,7 @@ 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.TargetingRequestExecutor; 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; @@ -35,19 +35,19 @@ public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { public static final String CODE = "optable-targeting-raw-auction-request-hook"; private final ConfigResolver configResolver; - private final NetworkCall networkCall; + private final TargetingRequestExecutor targetingRequestExecutor; private final BidderEnrichmentSampler bidderEnrichmentSampler; private final CompositeHookExecutionPlan hooksExecutionPlan; private final double logSamplingRate; public OptableRawAuctionRequestHook(ConfigResolver configResolver, - NetworkCall networkCall, + TargetingRequestExecutor targetingRequestExecutor, BidderEnrichmentSampler bidderEnrichmentSampler, CompositeHookExecutionPlan hooksExecutionPlan, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); - this.networkCall = Objects.requireNonNull(networkCall); + this.targetingRequestExecutor = Objects.requireNonNull(targetingRequestExecutor); this.bidderEnrichmentSampler = Objects.requireNonNull(bidderEnrichmentSampler); this.hooksExecutionPlan = hooksExecutionPlan; this.logSamplingRate = logSamplingRate; @@ -89,7 +89,7 @@ public Future> call(AuctionRequestPayloa final long crossHookFutureTimeout = hooksExecutionPlan.getOptableTargetingBidderRequestTimeout(account); - final Future optableTargetingCall = networkCall.makeRequest( + final Future optableTargetingCall = targetingRequestExecutor.makeRequest( payload, invocationContext, properties, 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 c01496e637d..c968debfa88 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,7 +11,7 @@ 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.TargetingRequestExecutor; 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; @@ -37,18 +37,18 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuc "Account not properly configured: tenant and/or origin is missing."; private final ConfigResolver configResolver; - private final NetworkCall networkCall; + private final TargetingRequestExecutor targetingRequestExecutor; private final double logSamplingRate; private final CompositeHookExecutionPlan hooksExecutionPlan; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, - NetworkCall networkCall, + TargetingRequestExecutor targetingRequestExecutor, CompositeHookExecutionPlan hooksExecutionPlan, double logSamplingRate) { this.configResolver = Objects.requireNonNull(configResolver); - this.networkCall = Objects.requireNonNull(networkCall); + this.targetingRequestExecutor = Objects.requireNonNull(targetingRequestExecutor); this.hooksExecutionPlan = hooksExecutionPlan; this.logSamplingRate = logSamplingRate; } @@ -142,7 +142,7 @@ private Future resolvePreEarlyNetworkCall( return Future.failedFuture(AUCTION_NOT_PROPERLY_CONFIGURED); } - return networkCall.makeRequest( + return targetingRequestExecutor.makeRequest( payload, invocationContext, properties, 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/TargetingRequestExecutor.java similarity index 97% rename from extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java rename to extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 967f9d36cea..36cd33c7724 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/TargetingRequestExecutor.java @@ -24,13 +24,13 @@ import java.util.Objects; -public class NetworkCall { +public class TargetingRequestExecutor { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; private final TimeoutFactory timeoutFactory; - public NetworkCall(OptableTargeting optableTargeting, + public TargetingRequestExecutor(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask, TimeoutFactory timeoutFactory) { 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 4da8ef24068..059db74ce0e 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 @@ -20,7 +20,7 @@ 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.TargetingRequestExecutor; 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; @@ -55,7 +55,7 @@ public class OptableRawAuctionRequestHookTest extends BaseOptableTest { private BidderEnrichmentSampler bidderEnrichmentSampler; private ConfigResolver configResolver; - private NetworkCall networkCall; + private TargetingRequestExecutor targetingRequestExecutor; private OptableRawAuctionRequestHook target; @BeforeEach @@ -63,9 +63,9 @@ 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, timeoutFactory); + targetingRequestExecutor = new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); target = new OptableRawAuctionRequestHook( - configResolver, networkCall, bidderEnrichmentSampler, + configResolver, targetingRequestExecutor, bidderEnrichmentSampler, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); when(invocationContext.timeout()).thenReturn(timeout); @@ -121,7 +121,7 @@ public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAc configResolver = new ConfigResolver( mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true)); target = new OptableRawAuctionRequestHook( - configResolver, networkCall, bidderEnrichmentSampler, + configResolver, targetingRequestExecutor, bidderEnrichmentSampler, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); // when @@ -158,7 +158,7 @@ public void shouldNotInjectEarlyNetworkCallWhenTrafficSourceIsInvalid(VertxTestC configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); target = new OptableRawAuctionRequestHook( - configResolver, networkCall, bidderEnrichmentSampler, + configResolver, targetingRequestExecutor, bidderEnrichmentSampler, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); // when 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 ab63e820cd0..7387e8f85d5 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 @@ -27,7 +27,7 @@ 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.TargetingRequestExecutor; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -71,7 +71,7 @@ class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptableTest { @Mock(strictness = Mock.Strictness.LENIENT) private TimeoutFactory timeoutFactory; - private NetworkCall networkCall; + private TargetingRequestExecutor targetingRequestExecutor; private OptableTargetingProcessedAuctionRequestHook target; @@ -80,9 +80,9 @@ 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, timeoutFactory); + targetingRequestExecutor = new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); + configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn( @@ -161,14 +161,20 @@ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { // given final ModuleContext moduleContext = new ModuleContext(); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, false)), 0.01); + configResolver, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(givenExecutionPlan(true, false)), + 0.01); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); when(invocationContext.moduleContext()).thenReturn(moduleContext); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); moduleContext.setOptableTargetingCall( - networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( - "key", "tenant", "origin", false), null)); + targetingRequestExecutor.makeRequest( + auctionRequestPayload, + invocationContext, + givenOptableTargetingProperties("key", "tenant", "origin", false), + null)); // when final Future> future = target.call(auctionRequestPayload, @@ -201,7 +207,10 @@ void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { // given target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, false)), 0.01); + configResolver, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(givenExecutionPlan(false, false)), + 0.01); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); @@ -237,7 +246,10 @@ void callShouldReturnResultWithEnrichedBidRequestWhenBothHooksAreAbsent() { void callShouldReturnResultWithEnrichedBidRequestWhenOnlyBidderRequestHookIsPresent() { // given target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(false, true)), 0.01); + configResolver, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(givenExecutionPlan(false, true)), + 0.01); when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); when(optableTargeting.getTargeting(any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); @@ -273,7 +285,10 @@ void callShouldReturnResultWithEnrichedBidRequestWhenOnlyBidderRequestHookIsPres void callShouldReturnResultWithoutEnrichedBidRequestWhenBothHooksArePresent() { // given target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(givenExecutionPlan(true, true)), 0.01); + configResolver, + targetingRequestExecutor, + CompositeHookExecutionPlan.of(givenExecutionPlan(true, true)), + 0.01); when(invocationContext.moduleContext()).thenReturn(new ModuleContext()); // when @@ -305,7 +320,7 @@ void callShouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); + configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -334,7 +349,7 @@ void callShouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { jsonMerger, givenOptableTargetingProperties("key", null, "origin", false)); target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, networkCall, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); + configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); From a32c06c6a2ef5bf65b428a1f0989c1ee168d9e57 Mon Sep 17 00:00:00 2001 From: softcoder Date: Fri, 5 Jun 2026 18:03:44 +0200 Subject: [PATCH 26/30] optable-targeting: code cleanup --- .../v1/OptableRawAuctionRequestHook.java | 2 +- ...eTargetingProcessedAuctionRequestHook.java | 5 ++ .../v1/core/CompositeHookExecutionPlan.java | 52 ++++++++----------- 3 files changed, 27 insertions(+), 32 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 79102d98bbb..e5b24f3c53c 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 @@ -100,7 +100,7 @@ public Future> call(AuctionRequestPayloa return update(BidRequestCleaner.instance(), moduleContext); } - public static Future> update( + public 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/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index c968debfa88..dfa8520f157 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 @@ -26,6 +26,11 @@ import java.util.Objects; +/** + * @deprecated This hook is deprecated and will be removed in a future release. + * Use {@link OptableRawAuctionRequestHook} and {@link OptableBidderRequestHook} instead. + */ +@Deprecated public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( 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 280e9b2192d..05c3a1ad2cf 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 @@ -14,6 +14,7 @@ import java.util.List; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; public class CompositeHookExecutionPlan { @@ -53,45 +54,34 @@ public static CompositeHookExecutionPlan of(ExecutionPlan globalExecutionPlan) { STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST)); } - public boolean hasRawAuctionRequestHook(Account account) { + private T computeFromAccount(Account account, + ConcurrentHashMap cache, + T defaultValue, + Function compute) { final String accountId = account != null ? account.getId() : null; - return StringUtils.isNotEmpty(accountId) - ? rawAuctionRequestHookCache.computeIfAbsent(accountId, id -> { - final ExecutionPlan accountSpecificHooksExecutionPlan = resolveExecutionPlan(account); - return hasHook( - accountSpecificHooksExecutionPlan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) - || hasGlobalRawAuctionRequestHook; - }) - : false; + ? cache.computeIfAbsent(accountId, id -> compute.apply(resolveExecutionPlan(account))) + : defaultValue; } - public boolean hasBidderRequestHook(Account account) { - final String accountId = account != null ? account.getId() : null; + public boolean hasRawAuctionRequestHook(Account account) { + return computeFromAccount(account, rawAuctionRequestHookCache, false, + plan -> hasHook(plan, STAGE_RAW_AUCTION_REQUEST, HOOK_CODE_OPTABLE_RAW_AUCTION) + || hasGlobalRawAuctionRequestHook); + } - 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(Account account) { + return computeFromAccount(account, bidderRequestHookCache, false, + plan -> hasHook(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST) + || hasGlobalBidderRequestHook); } public long getOptableTargetingBidderRequestTimeout(Account account) { - final String accountId = account != null ? account.getId() : null; - - return StringUtils.isNotEmpty(accountId) - ? bidderRequestHookTimeoutCache.computeIfAbsent(accountId, id -> { - final ExecutionPlan accountSpecificHoksExecutionPlan = resolveExecutionPlan(account); - final long hookTimeOut = getHookTimeout( - accountSpecificHoksExecutionPlan, - STAGE_BIDDER_REQUEST, - HOOK_CODE_OPTABLE_BIDDER_REQUEST); - return hookTimeOut != 0 ? hookTimeOut : globalBidderRequestHookTimeout; - }) - : globalBidderRequestHookTimeout; + return computeFromAccount(account, bidderRequestHookTimeoutCache, globalBidderRequestHookTimeout, + plan -> { + final long timeout = getHookTimeout(plan, STAGE_BIDDER_REQUEST, HOOK_CODE_OPTABLE_BIDDER_REQUEST); + return timeout != 0 ? timeout : globalBidderRequestHookTimeout; + }); } private ExecutionPlan resolveExecutionPlan(Account account) { From 2d956b14450f71116965ce29939835382287ec64 Mon Sep 17 00:00:00 2001 From: softcoder Date: Wed, 22 Jul 2026 19:20:00 +0200 Subject: [PATCH 27/30] optable-targeting: fix endpoint template --- sample/configs/prebid-config-with-optable.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index 17ec6a77be9..77514babf26 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -52,4 +52,4 @@ hooks: enabled: true modules: optable-targeting: - api-endpoint: https://ca.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}} + api-endpoint: https://ca.edge.optable.co/v2/targeting?t={TENANT}&o={ORIGIN} From 3956b653c29195830fb2e953129b56d55eabb74b Mon Sep 17 00:00:00 2001 From: softcoder Date: Fri, 31 Jul 2026 13:47:38 +0200 Subject: [PATCH 28/30] optable-targeting: fix per bidder enrichment if only enrichment-percantage property is specified and it's value is 100% --- .../targeting/model/config/OptableTargetingProperties.java | 6 +----- .../optable/targeting/v1/OptableBidderRequestHook.java | 3 --- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 4db54642903..683ea1c8eae 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,7 +3,6 @@ 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; @@ -44,6 +43,7 @@ public final class OptableTargetingProperties { CacheProperties cache = new CacheProperties(); + @JsonProperty("enrichment-percentage") Integer enrichmentPercentage = 100; @JsonProperty("bidder-enrichment-percentages") @@ -52,8 +52,4 @@ public final class OptableTargetingProperties { 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/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index 7c544a47126..b5ce4cd7531 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,9 +33,6 @@ public Future> call(BidderRequestPayload final ModuleContext moduleContext = ModuleContext.of(invocationContext); final OptableTargetingProperties properties = moduleContext.getOptableTargetingProperties(); - if (!properties.isPerBidderEnrichmentEnabled()) { - return noAction(moduleContext, null); - } final Set biddersToEnrich = moduleContext.getBiddersToEnrich(); if (CollectionUtils.isEmpty(biddersToEnrich) From e456f179fe18b7cfed0aaae803f27fdea5d5f62e Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 31 Jul 2026 16:05:21 +0200 Subject: [PATCH 29/30] optable-targeting: bind enrich-web and enrich-app from account config The account config node is deserialized with the prebid mapper, which uses SNAKE_CASE, so the unannotated fields bound to enrich_web and enrich_app while the documented account keys are enrich-web and enrich-app. Per-account overrides of both were silently dropped and the global value kept. --- .../targeting/model/config/OptableTargetingProperties.java | 2 ++ 1 file changed, 2 insertions(+) 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 683ea1c8eae..15ed1d2012b 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,7 +49,9 @@ public final class OptableTargetingProperties { @JsonProperty("bidder-enrichment-percentages") Map bidderEnrichmentPercentages = Map.of(); + @JsonProperty("enrich-web") Boolean enrichWeb = true; + @JsonProperty("enrich-app") Boolean enrichApp = true; } From 87657141d88b7fb72a0c50afa4011bb19a43a408 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 14 Aug 2026 00:29:18 +0200 Subject: [PATCH 30/30] optable-targeting: fix execution plan endpoint lookup after core API change Core changed ExecutionPlan.endpoints key type from Endpoint to HookHttpEndpoint. Map.get accepts Object, so the stale lookup kept compiling but always returned null against the EnumMap: the module read a bidder-request hook timeout of 0, so the targeting API call inherited only the raw-auction-request stage budget (50ms) instead of the configured 500ms and timed out on every auction. --- .../targeting/v1/core/CompositeHookExecutionPlan.java | 8 ++++---- .../OptableTargetingProcessedAuctionRequestHookTest.java | 4 ++-- .../targeting/v1/core/CompositeHookExecutionPlanTest.java | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) 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 05c3a1ad2cf..0d5bda38be9 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 @@ -4,11 +4,11 @@ import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.HookHttpEndpoint; import org.prebid.server.hooks.execution.model.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.model.Endpoint; import org.prebid.server.settings.model.Account; import java.util.List; @@ -18,7 +18,7 @@ public class CompositeHookExecutionPlan { - private static final String ENDPOINT_AUCTION = "openrtb2_auction"; + private static final HookHttpEndpoint ENDPOINT_AUCTION = HookHttpEndpoint.POST_AUCTION; private static final String STAGE_RAW_AUCTION_REQUEST = "raw_auction_request"; private static final String STAGE_BIDDER_REQUEST = "bidder_request"; private static final String HOOK_CODE_OPTABLE_RAW_AUCTION = OptableRawAuctionRequestHook.CODE; @@ -94,7 +94,7 @@ private ExecutionPlan resolveExecutionPlan(Account account) { 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(endpoints -> endpoints.get(ENDPOINT_AUCTION)) .map(EndpointExecutionPlan::getStages) .map(stages -> stages.get(Stage.valueOf(stage))) .map(StageExecutionPlan::getGroups) @@ -108,7 +108,7 @@ private static boolean hasHook(ExecutionPlan executionPlan, String stage, String 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(endpoints -> endpoints.get(ENDPOINT_AUCTION)) .map(EndpointExecutionPlan::getStages) .map(stages -> stages.get(Stage.valueOf(stage))) .map(StageExecutionPlan::getGroups) 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 7387e8f85d5..894c8082175 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 @@ -19,6 +19,7 @@ import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.HookHttpEndpoint; import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; @@ -34,7 +35,6 @@ import org.prebid.server.hooks.v1.InvocationStatus; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; import java.util.List; @@ -462,6 +462,6 @@ private ExecutionPlan givenExecutionPlan(boolean hasRawAuctionRequestHook, boole Stage.bidder_request, bidderRequestStage )); - return ExecutionPlan.of(null, Map.of(Endpoint.openrtb2_auction, endpointExecutionPlan)); + return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_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 index 975ecf37ccf..c836de388a9 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 @@ -4,10 +4,10 @@ import org.prebid.server.hooks.execution.model.EndpointExecutionPlan; import org.prebid.server.hooks.execution.model.ExecutionGroup; import org.prebid.server.hooks.execution.model.ExecutionPlan; +import org.prebid.server.hooks.execution.model.HookHttpEndpoint; import org.prebid.server.hooks.execution.model.HookId; import org.prebid.server.hooks.execution.model.Stage; import org.prebid.server.hooks.execution.model.StageExecutionPlan; -import org.prebid.server.model.Endpoint; import org.prebid.server.settings.model.Account; import org.prebid.server.settings.model.AccountHooksConfiguration; @@ -339,7 +339,7 @@ private ExecutionPlan givenExecutionPlan(String stage, String 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)); + return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_AUCTION, endpointPlan)); } private ExecutionPlan givenExecutionPlanWithTimeout(String stage, String hookCode, long timeout) { @@ -347,7 +347,7 @@ private ExecutionPlan givenExecutionPlanWithTimeout(String stage, String hookCod 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)); + return ExecutionPlan.of(null, Map.of(HookHttpEndpoint.POST_AUCTION, endpointPlan)); } private Account givenAccount(String accountId, ExecutionPlan executionPlan) {