From 11b1195026e4fcda69646ace594810b3dc0f24b6 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 4 May 2026 19:13:16 +0200 Subject: [PATCH 01/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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/56] 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) { From 1534b22bb1f74ddc9cd429720cfaf36b0e48fa2f Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 23 Jul 2026 22:07:21 +0200 Subject: [PATCH 31/56] optable-targeting: add hid (resolver hint) support --- .cursorrules | 262 ++++++++++++++++++ .gemini/settings.json | 15 + GEMINI.md | 9 + extra/.vscode_old/launch.json | 36 +++ extra/.vscode_old/settings.json | 18 ++ extra/.vscode_old/tasks.json | 33 +++ .../optable/targeting/model/Query.java | 4 +- .../config/OptableTargetingProperties.java | 3 + .../optable/targeting/v1/core/IdsMapper.java | 5 + .../targeting/v1/core/OptableTargeting.java | 2 +- .../targeting/v1/core/QueryBuilder.java | 41 ++- .../optable/targeting/v1/BaseOptableTest.java | 2 +- .../targeting/v1/core/IdsMapperTest.java | 14 +- .../targeting/v1/core/QueryBuilderTest.java | 92 +++++- .../configs/sample-app-settings-optable.yaml | 3 +- 15 files changed, 520 insertions(+), 19 deletions(-) create mode 100644 .cursorrules create mode 100644 .gemini/settings.json create mode 100644 GEMINI.md create mode 100644 extra/.vscode_old/launch.json create mode 100644 extra/.vscode_old/settings.json create mode 100644 extra/.vscode_old/tasks.json diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 00000000000..9d6f12e9da7 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,262 @@ +# Prebid Server Java Coding Rules + +You are an expert Java developer and Professor of Software Engineering, specializing in the Prebid Server Java codebase. You adhere to strict high-quality standards, favoring Modern Java (17+), Vert.x patterns, JUnit 5, Mockito (BDD style), and AssertJ. + +## 1. General Coding Philosophy +- **Non-Blocking I/O**: This is a Vert.x application. NEVER block the Event Loop. Use asynchronous patterns (`Future`, `Promise`) for I/O operations. +- **Immutability**: Prefer immutable data structures. + - Use `final` for all local variables, fields, and parameters. + - **Modern Java:** Use Java 17+ features like `record` and `sealed interface` where appropriate for data carriers and hierarchy restrictions. + - **Var Usage:** `var` is **STRICTLY PROHIBITED** in production code. Use explicit types. `var` is ALLOWED in tests. + - **Final Keyword:** Use `final` for all variables and parameters where possible to ensure immutability. + - **Switch Expressions:** Use modern `switch` expressions with arrow syntax `->`. + - **DTOs / POJOs**: **STRICTLY PROHIBITED** to write manual Getters, Setters, `toString`, `equals`, `hashCode`, or Constructors if Lombok can handle it. + - Use `@Value` + `staticConstructor="of"` for immutable value objects (default). + - Use `@Builder(toBuilder = true)` for complex objects (>4 fields) or when mutation-like copies are needed. + - Use `@AllArgsConstructor` / `@RequiredArgsConstructor` for simple dependency injection or wrappers. +- **Variable Types**: **DO NOT USE `var`**. Always write full variable types (Project Convention). +- **Null Safety**: + - Avoid returning `null`. Use `Optional` for return types. + - Avoid long chains of null checks; use `Optional` or `org.prebid.server.util.ObjectUtil`. +- **Privacy**: **STRICTLY PROHIBITED** to log private data (publishers, exchanges, usage analytics). +- **JSON Handling**: + - **FORBIDDEN**: `io.vertx.core.json.Json` (static Vert.x mapper). + - **ALLOWED**: Inject and use `JacksonMapper` or the project's configured `ObjectMapper`. + +## 3. Libraries & Utilities +- **Apache Commons**: Prefer using existing helpers from `commons-lang3` and `commons-collections4` over writing custom utility methods. + - Used: `StringUtils`, `ObjectUtils`, `CollectionUtils`, `MapUtils`. + - Avoid creating new `*Util` classes if an Apache Commons equivalent exists. + - Example: Use `StringUtils.isBlank(str)` instead of `str == null || str.trim().isEmpty()`. +- **Project Utilities**: + - **Project Utilities:** Use `org.prebid.server.util.ObjectUtil.getIfNotNull()` for concise, null-safe access chains (alternative to `Optional` in hot paths). + - **Apache Commons:** Heavily prefer `StringUtils`, `ObjectUtils`, `CollectionUtils`, `ListUtils`, `MapUtils` for null-safe operations. + - **Validation:** Use `java.util.Objects.requireNonNull(arg)` in constructors to enforce non-null dependencies. + - **Collection Utilities:** Use `java.util.Collections.emptyList()`, `Map.of()`, `Set.of()` etc. for better readability and immutability. + + +## 2. Code Style & Standards (Oracle & Checkstyle) +- **Base Standard**: Adhere to [Oracle Java Code Conventions](https://www.oracle.com/docs/tech/java/codeconventions.pdf) unless overridden by project-specific Checkstyle rules. +- **Indentation**: + - **4 spaces** for class members, methods, and blocks. + - **8 spaces** for line continuations (wrapping). +- **Line Length**: Max 120 characters (overrides Oracle's 80). +- **Class Structure** (Oracle Convention): + 1. Class/Instance Variables (Standard Order: public, protected, package, private). + 2. Constructors. + 3. Methods (Grouped by functionality, logic flow, or readability). +- **Declarations**: + - One variable declaration per line. + - Initialize variables at declaration where possible. + - Place declarations at the beginning of blocks (legacy Oracle) OR near first usage (Modern/Clean Code) -> **Prefer near first usage**. +- **Statements**: + - Always use braces `{}` for `if`, `else`, `for`, `do`, `while` (K&R style). + - No parentheses in `return` statements. + +- **Imports**: + - **FORBIDDEN**: Wildcard imports (`import java.util.*;`). + - Order: Third-party libraries first, then `java.*`/`jakarta.*` at the bottom (separated by blank line). + - No `static` imports in production code (except standard utilities if really needed), but allowed in Tests. + - **Illegal Imports**: `org.junit.Test` (JUnit 4), `org.apache.commons.lang` (use `lang3`), `io.vertx.core.json.Json`. +- **Naming**: + - Constants: `UPPER_CASE_WITH_UNDERSCORES`. + - Methods/Fields: `camelCase`. + - **Maps**: Use `keyToValue` convention (e.g., `impToExt`, `accountIdToBidder`). + - **Self-Explanatory**: Avoid single-letter variables. `resolvedParam` > `s`. +- **Collections**: + - Use literals/factories: `List.of()`, `Collections.emptyList()`, `Collections.singletonList()`. +- **Formatting**: + - Parenthesis on expression end. + - Ternary operators: Long ones on separate lines, short ones on one line. + - Boolean logic: Explicit parenthesis for precedence `(a && b) || c`. + - **Method Ordering**: Call order (Interface method -> private methods it calls -> next Interface method). +- **Dependencies**: Do not call methods from transitive dependencies; declare them explicitly in `pom.xml`. +- **Lombok**: + - Use `@Value` + `staticConstructor="of"`. + - Use `@Builder` for constructors with > 4 arguments. + - `toBuilder = true` for updates. + +## 4. Architecture, SOLID & Design Patterns + +### 4.1 SOLID Principles +- **SRP (Single Responsibility)**: + - Classes must have one clearly defined purpose. + - **Refactor**: Split large "God Classes" into smaller delegates or services. +- **OCP (Open/Closed)**: + - Design for extension. Use Interfaces and Strategy patterns so new behavior can be added without changing existing code. +- **LSP (Liskov Substitution)**: + - Subclasses/Implementations must behave consistently. Do not throw `UnsupportedOperationException` unexpectedly. +- **ISP (Interface Segregation)**: + - Prefer focused interfaces (e.g., `Reader`, `Writer`) over large broad ones. +- **DIP (Dependency Inversion)**: + - Depend on abstractions (Interfaces). + - **Framework**: Use **Spring Framework** for Dependency Injection. + - **Injection Style**: **Constructor Injection** is REQUIRED. Field injection (`@Autowired` on fields) is **STRICTLY PROHIBITED**. + +### 4.2 Clean Code & Best Practices +- **Readability**: Code is read more often than written. Optimize for reading. +- **Naming**: + - **Intent-Revealing**: `daysSinceCreation` > `d`. + - **No Encodings**: No Hungarian notation or type prefixes. +- **Functions**: + - **Small**: Methods should be small and do one thing. + - **Guard Clauses**: Use strict guard clauses/early returns to flatten nesting. + - **Pure Functions**: Prefer `private static` methods for stateless logic. +- **Comments**: + - **Why, not What**: Comments should explain the business decision, not the syntax. + - **No Code Comments**: Do not comment out code; delete it. Git history remembers. + +### 4.3 Architecture & Reactive Vert.x Patterns +- **Reactive Philosophy**: + - **Everything is a Stream/Future**: Treat business logic as a pipeline of transformation steps. + - **Chaining**: Build flows by chaining operator methods (`map`, `compose`, `onContentType`, `recover`). + - **No Callbacks**: "Callback Hell" is strictly prohibited. Use functional composition. +- **Future Composition (The Glue)**: + - **Strict Transformation Pipeline:** Treat business logic as a stream of data transformations. + - **Chaining:** Use `.map()` for synchronous transformations and `.compose()` for asynchronous operations. + - **Parallelism:** Use `Future.all()`, `CompositeFuture.join()`, or `CompositeFuture.all()` for parallel execution. + - **Side Effects:** Use `.onSuccess()`, `.onFailure()`, and `.onComplete()` **ONLY** for side effects (logging, metrics). **NEVER** use them for control flow or chaining logic. + - **Error Handling:** Propagate errors down the chain. Use `.recover()` for falling back or transforming errors. + - **Avoid Callbacks:** **STRICTLY PROHIBIT** nested callbacks or "Callback Hell". logic must be flat. + - **Thread Safety:** Always assume the code runs on the Event Loop. **NEVER** block the thread. + - **Context Awareness:** Pass `RoutingContext` or `AuctionContext` as a carrier of state through the chain. + + ### 4.4 Object-Oriented Patterns + - **Value Objects:** Use Lombok `@Value(staticConstructor = "of")` for immutable data carriers. + - **Factory Methods:** Prefer static factory methods named `of(...)` or `create(...)` over public constructors for complex object creation. + - **NoOp Pattern:** For interfaces that may have empty implementations (e.g., hooks, empty services), create a `NoOpExtension` or `static NoOp` implementation within the interface or as a separate class. + - **Sealed Hierarchies:** Use `sealed interface` and `record` (Java 17+) for restricted class hierarchies (e.g., specialized result types). + + ### 4.5 JSON & Serialization + - **JacksonMapper:** Always use the project's `JacksonMapper` wrapper instead of raw `ObjectMapper` where possible. + - **Dynamic JSON:** Use `ObjectNode` and `ArrayNode` for handling dynamic or unstructured data (especially `ext` fields) rather than untyped `Map`. + - **JsonPointer:** Use `JsonPointer` for safe and readable deep traversal of JSON trees (`node.at("/path/to/field")`). +- **Concurrency (Vert.x Core Rule)**: + - **Single Threaded Event Loop**: The application runs on the Event Loop. + - **PROHIBITED**: `synchronized`, `wait()`, `notify()`, `Thread.sleep()`, `BlockingQueue`, or any blocking Java concurrency primitive. + - **Blocking Code**: If you MUST run blocking code (e.g. legacy JDBC), use `vertx.executeBlocking(...)` but prefer non-blocking clients. + - **ALLOWED**: `Future`, `Promise`, `vertx.setTimer()`. +- **Error Handling**: + - **Async**: In async chains, errors propagate automatically. Do not break the chain. + - **Return Failed Future**: In async methods, return `Future.failedFuture(e)` immediately rather than throwing runtime exceptions. + - **Granularity**: Catch specific exceptions in `.recover()`. Avoid broad catches unless it's a top-level handler. +- **Context**: Be aware of the Vert.x `Context`. Ensure context is preserved when switching threads. +- **Logging**: + - Use standard SLF4J usages: `private static final Logger logger = LoggerFactory.getLogger(MyClass.class);` + - Do not use `System.out.println` or `e.printStackTrace()`. + - Log at `DEBUG` for high-volume messages, `INFO` for lifecycle events, `WARN/ERROR` for unexpected conditions. + +## 5. Testing Rules +- **Frameworks**: + - **JUnit 5**: `org.junit.jupiter.api.*`. + - **Behavior Driven Development (BDD)**: Use `given(mock.method()).willReturn(...)` instead of `when(...)`. + - **Strictness**: Use `@Mock(strictness = Mock.Strictness.LENIENT)` for infrastructure mocks (Metrics, Services) defined in `setUp` but not used in every test to avoid `UnnecessaryStubbingException`. + - **Async Testing**: For `Future`-based tests, use `VertxTestContext`: + ```java + @Test + void shouldSucceed(VertxTestContext context) { + future.onComplete(context.succeeding(result -> { + assertThat(result).isNotNull(); + context.completeNow(); + })); + } + ``` + - **Time Testing**: Mock `java.time.Clock` ensures deterministic time tests. + - **Static Imports**: ALWAYS statically import: + - `org.assertj.core.api.Assertions.*` (assertThat, tuple, entry) + - `org.mockito.BDDMockito.*` (given) + - `org.mockito.Mockito.*` (verify, never, etc.) + - `org.mockito.ArgumentMatchers.*` (any, eq, etc.) + - **Assertions**: + - Use `extracting()` for nested properties. + - Use `containsExactlyInAnyOrder` for lists. + - Use `containsOnly` for verifying single-element logical containment. + - **Structure**: + - Use `target` as the name for the class under test instance. + - Use `setUp()` method annotated with `@BeforeEach`. + - **Vert.x Data**: Use `MultiMap.caseInsensitiveMultiMap()` for testing headers/params. + - Fields: SUT (System Under Test) named `target`. +- **Granularity**: + - 1 Test = 1 Logic Path. Avoid `testFooAndBar`. Split into `testFoo` and `testBar`. + - No `ParameterizedTest` preference; explicitly write separate tests for meaningful scenarios (per docs). +- **Data Placement**: + - **Inline Data**: Place test data INSIDE the test method (local variables). + - **No Constants**: Avoid class-level constants for test data. + - **Fake Data**: Do not use real URLs/IDs (use `test.com`, `id`). +- **Mocking**: + - Annotate test class with `@ExtendWith(MockitoExtension.class)`. + - Use `@Mock` for dependencies. + - Use strict mocking (default). Only use `lenientness` if absolutely necessary for shared setup. +- **Naming**: `methodNameShouldReturnExpectedBehaviorWhenCondition`. + - Example: `processDataShouldReturnResultWhenInputIsData`. +- **BDD Style**: + ```java + // given + given(dependency.call()).willReturn(futureResult); + final var input = "test-input"; // 'var' is acceptable in tests only if brevity aids readability, but prefer explicit types per project rule. + + // when + Future result = target.execute(input); + + // then + assertThat(result.succeeded()).isTrue(); + ``` + +## 6. Implementation Workflow (Agent Instructions) +1. **Analyze**: Read existing code and `pom.xml` to understand dependencies. +2. **Plan**: Propose changes before writing. +3. **Implement**: + - Write logic using functional style (`Stream` API). + - Ensure extensive logging for debuggability (at `debug` or `trace` level for high-volume paths). +4. **Test**: + - ALWAYS write or update unit tests for changed logic. + - Validate logic with `VertxTest` if async/JSON is involved. + +## 7. Example Test Template + +```java +package org.prebid.server.component; + +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.VertxTest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.verify; + +@ExtendWith(MockitoExtension.class) +class ExampleTest { + + @Mock + private Dependency dependency; + @Mock(strictness = Mock.Strictness.LENIENT) // Common infra mock + private Metrics metrics; + + private Example target; + + @BeforeEach + void setUp() { + target = new Example(dependency, metrics); + } + + @Test + void shouldReturnExpectedValueWhenConditionMet() { + // given + given(dependency.getData()).willReturn("data"); + + // when + final var result = target.process(); + + // then + assertThat(result) + .extracting(Result::getValue) + .isEqualTo("data"); + verify(metrics).updateMetric(any()); + } +} +``` diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 00000000000..44d4be1fc97 --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "BeforeTool": [ + { + "matcher": "read_file|list_directory", + "hooks": [ + { + "type": "command", + "command": "python -c \"import sys,pathlib,json;e=pathlib.Path('graphify-out/graph.json').exists();d={'decision':'allow'};e and d.update({'additionalContext':'graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.'});sys.stdout.write(json.dumps(d))\"" + } + ] + } + ] + } +} \ No newline at end of file diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 00000000000..f449b672b54 --- /dev/null +++ b/GEMINI.md @@ -0,0 +1,9 @@ +## graphify + +This project has a graphify knowledge graph at graphify-out/. + +Rules: +- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure +- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files +- For cross-module "how does X relate to Y" questions, prefer `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files +- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/extra/.vscode_old/launch.json b/extra/.vscode_old/launch.json new file mode 100644 index 00000000000..3641a02708c --- /dev/null +++ b/extra/.vscode_old/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Debug Prebid Server (default config)", + "request": "launch", + "mainClass": "org.prebid.server.Application", + "projectName": "prebid-server-bundle", + "args": [ + "--spring.config.additional-location=sample/configs/prebid-config-with-optable.yaml" + ], + "vmArgs": "-Xmx2G -Dlogging.level.root=INFO" + }, + { + "type": "java", + "name": "Debug Prebid Server (custom YAML)", + "request": "launch", + "mainClass": "org.prebid.server.Application", + "projectName": "prebid-server", + "args": [ + "--spring.config.additional-location=config/my-prebid.yaml" + ], + "vmArgs": "-Xmx2G -Dlogging.level.root=DEBUG" + }, + { + "type": "java", + "name": "Debug Prebid Server (no args)", + "request": "launch", + "mainClass": "org.prebid.server.Application", + "projectName": "prebid-server", + "vmArgs": "-Xmx1G" + } + ] + } + \ No newline at end of file diff --git a/extra/.vscode_old/settings.json b/extra/.vscode_old/settings.json new file mode 100644 index 00000000000..6426ceea05a --- /dev/null +++ b/extra/.vscode_old/settings.json @@ -0,0 +1,18 @@ +{ + "java.configuration.maven.pomFile": "extra/pom.xml", + "java.import.maven.enabled": true, + "java.autobuild.enabled": true, + "java.configuration.updateBuildConfiguration": "automatic", + "java.errors.incompleteClasspath.severity": "ignore", + "files.watcherExclude": { + "**/target/**": true + }, + "java.completion.importOrder": [ + "#", + "java" + ], + "editor.codeActionsOnSave": { + "source.organizeImports": "never" + } + } + \ No newline at end of file diff --git a/extra/.vscode_old/tasks.json b/extra/.vscode_old/tasks.json new file mode 100644 index 00000000000..aa0aab32b13 --- /dev/null +++ b/extra/.vscode_old/tasks.json @@ -0,0 +1,33 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "Maven: Fetch dependencies", + "type": "shell", + "command": "mvn -q dependency:go-offline", + "group": "build", + "problemMatcher": [] + }, + { + "label": "Maven: Build (full package)", + "type": "shell", + "command": "mvn -q clean package -DskipTests", + "group": "build", + "problemMatcher": "$maven" + }, + { + "label": "Maven: Test", + "type": "shell", + "command": "mvn -q test", + "group": "test", + "problemMatcher": "$maven" + }, + { + "label": "Format Java (Google Style)", + "type": "shell", + "command": "mvn -q spotless:apply", + "problemMatcher": [] + } + ] + } + \ No newline at end of file diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java index 20862050f39..19b725ab666 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -7,9 +7,11 @@ public class Query { String ids; + String hid; + String attributes; public String toQueryString() { - return ids + attributes; + return ids + hid + attributes; } } 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 15ed1d2012b..314e2639cef 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 @@ -32,6 +32,9 @@ public final class OptableTargetingProperties { @JsonProperty("id-prefix-order") String idPrefixOrder; + @JsonProperty("hid-prefixes") + String hidPrefixes; + @JsonProperty("optable-inserter-eids-merge") Set optableInserterEidsMerge = Set.of(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index 7214f6ce6a3..a9b4c9c7311 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -81,6 +81,11 @@ private static void addDeviceIds(Map ids, Device device) { final String ifa = device != null ? device.getIfa() : null; final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null; final int lmt = Optional.ofNullable(device).map(Device::getLmt).orElse(0); + final String ip6 = device != null ? device.getIpv6() : null; + + if (ip6 != null) { + ids.put(Id.DEVICE_IP_V_6, ip6); + } if (ifa == null || StringUtils.isEmpty(os) || lmt == 1) { return; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 0cb16d9c456..b79ca2790f2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -29,7 +29,7 @@ public Future getTargeting(OptableTargetingProperties propertie Timeout timeout) { final List ids = idsMapper.toIds(bidRequest, properties.getPpidMapping()); - final Query query = QueryBuilder.build(ids, attributes, properties.getIdPrefixOrder()); + final Query query = QueryBuilder.build(ids, attributes, properties); if (query == null) { return Future.failedFuture("Can't get targeting"); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index bee89818b2f..63f4990f14c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -1,18 +1,22 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -26,12 +30,40 @@ public class QueryBuilder { private QueryBuilder() { } - public static Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { + public static Query build(List ids, OptableAttributes optableAttributes, + OptableTargetingProperties properties) { + if (CollectionUtils.isEmpty(ids) && CollectionUtils.isEmpty(optableAttributes.getIps())) { return null; } - return Query.of(buildIdsString(ids, idPrefixOrder), buildAttributesString(optableAttributes)); + final String idPrefixOrder = properties.getIdPrefixOrder(); + final String hidPrefixes = properties.getHidPrefixes(); + return Query.of( + buildIdsString(ids, idPrefixOrder), + buildHidString(ids, hidPrefixes), + buildAttributesString(optableAttributes)); + } + + private static String buildHidString(List ids, String hidPrefixesString) { + if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(hidPrefixesString)) { + return StringUtils.EMPTY; + } + final Map prefixToIdValue = ids.stream().collect(Collectors.toMap(Id::getName, it -> it)); + if (MapUtils.isEmpty(prefixToIdValue)) { + return StringUtils.EMPTY; + } + + final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) + .map(prefixToIdValue::get) + .filter(Objects::nonNull) + .map(it -> String.format("hid=%s:%s", it.getName(), it.getValue())) + .collect(Collectors.joining("&")); + if (StringUtils.isEmpty(hidParameters)) { + return StringUtils.EMPTY; + } + + return "&" + hidParameters; } private static String buildIdsString(List ids, String idPrefixOrder) { @@ -39,7 +71,10 @@ private static String buildIdsString(List ids, String idPrefixOrder) { return StringUtils.EMPTY; } - final List reorderedIds = reorderIds(ids, idPrefixOrder); + final List reorderedIds = reorderIds(ids, idPrefixOrder) + .stream() + .filter(id -> !Id.DEVICE_IP_V_6.equals(id.getName())) + .toList(); final StringBuilder sb = new StringBuilder(); for (Id id : reorderedIds) { 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 52143ff9fef..4bfb1947b5f 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 @@ -273,7 +273,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, } protected Query givenQuery() { - return Query.of("?que", "ry"); + return Query.of("?que", "r", "y"); } protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java index 39693661629..714fc538a34 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java @@ -56,7 +56,8 @@ public void shouldMapBidRequestToAllPossibleIds() { .doesNotContain(Id.of(Id.APPLE_IDFA, "ifa")) .contains(Id.of(Id.ID5, "id5_id")) .contains(Id.of(Id.UTIQ, "utiq_id")) - .contains(Id.of("c", "test_id")); + .contains(Id.of("c", "test_id")) + .contains(Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); } @Test @@ -71,6 +72,17 @@ public void shouldMapNothing() { assertThat(ids).isNotNull(); } + @Test + public void shouldMapIpv6WhenPresent() { + final BidRequest bidRequest = givenBidRequestWithEids(Map.of()); + + final List ids = target.toIds(bidRequest, Map.of()); + + assertThat(ids).isNotNull() + .contains(Id.of(Id.EMAIL, "email")) + .contains(Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); + } + private BidRequest givenBidRequestWithEids(Map eids) { final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); final ExtUser extUser = ExtUser.builder().build(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 16d5953ef71..9be6fc3a2b6 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -4,6 +4,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import java.util.List; import java.util.Set; @@ -16,16 +17,21 @@ public class QueryBuilderTest { private final String idPrefixOrder = "c,c1"; + private OptableTargetingProperties properties() { + return givenProperties(idPrefixOrder, null); + } + @Test public void shouldSeparateAttributesFromIds() { // given final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, optableAttributes, properties()); // then assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); + assertThat(query.getHid()).isEqualTo(""); assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); } @@ -35,10 +41,11 @@ public void shouldBuildFullQueryString() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, optableAttributes, properties()); // then assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); + assertThat(query.getHid()).isEqualTo(""); assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); assertThat(query.toQueryString()) .isEqualTo("&id=e%3Aemail&id=p%3A123&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); @@ -50,7 +57,7 @@ public void shouldBuildQueryStringWhenHaveIds() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString(); // then assertThat(query).contains("e%3Aemail", "p%3A123"); @@ -62,7 +69,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString(); // then assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); @@ -78,7 +85,7 @@ public void shouldBuildQueryStringWithRightOrder() { Id.of("c", "234")); // when - final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString(); // then assertThat(query).startsWith("&id=c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); @@ -93,7 +100,7 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { .build(); // when - final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, attributes, properties()); // then assertThat(query).isNotNull(); @@ -107,7 +114,7 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { final OptableAttributes attributes = OptableAttributes.builder().build(); // when - final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, attributes, properties()); // then assertThat(query).isNull(); @@ -124,7 +131,7 @@ public void shouldBuildQueryStringWithGppSid() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then assertThat(query).contains("&gpp=DBABzw~1YNY~BVQqAAAAAgA"); @@ -144,7 +151,7 @@ public void shouldBuildQueryStringWithSingleGppSid() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then assertThat(query).contains("&gpp_sid=7"); @@ -162,7 +169,7 @@ public void shouldLimitGppSidToTwoValues() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then final String gppSidValue = query.split("gpp_sid=")[1].split("&")[0]; @@ -181,12 +188,68 @@ public void shouldNotIncludeGppSidWhenEmpty() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then assertThat(query).doesNotContain("gpp_sid"); } + @Test + public void shouldBuildHidWhenHidPrefixesMatchIds() { + // given + final OptableTargetingProperties props = givenProperties(null, "c,i6"); + final List ids = List.of( + Id.of("c", "234"), + Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0:0:0:0:0:0:0:1"); + } + + @Test + public void shouldExcludeDeviceIpV6FromIdsString() { + // given + final OptableTargetingProperties props = givenProperties(null, "i6"); + final List ids = List.of( + Id.of(Id.EMAIL, "email"), + Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getIds()).doesNotContain(Id.DEVICE_IP_V_6); + assertThat(query.getHid()).isEqualTo("&hid=i6:0:0:0:0:0:0:0:1"); + } + + @Test + public void shouldNotBuildHidWhenNoMatch() { + // given + final OptableTargetingProperties props = givenProperties(null, "nonexistent"); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo(""); + } + + @Test + public void shouldNotBuildHidWhenHidPrefixesNotConfigured() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, properties()); + + // then + assertThat(query.getHid()).isEqualTo(""); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) @@ -194,4 +257,11 @@ private OptableAttributes givenOptableAttributes() { .gdprConsent("tcf") .build(); } + + private static OptableTargetingProperties givenProperties(String idPrefixOrder, String hidPrefixes) { + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setIdPrefixOrder(idPrefixOrder); + properties.setHidPrefixes(hidPrefixes); + return properties; + } } diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 4413a05769d..7b4af19a7e2 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -30,7 +30,8 @@ accounts: enrich-web: true enrich-app: true id-prefix-order: "e,v,c" - ppid-mapping: { "pubcid.org": "c" } + hid-prefixes: "c,i6" + ppid-mapping: { "pubmatic.com": "c" } adserver-targeting: true cache: enabled: false From f9125eb1e906bab1358236c1526bc93a3fe90e7a Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 23 Jul 2026 23:04:43 +0200 Subject: [PATCH 32/56] optable-targeting: add app bundle and ver to the Targeting API call --- .../modules/optable/targeting/model/App.java | 11 ++++ .../targeting/model/OptableAttributes.java | 2 + .../v1/core/OptableAttributesResolver.java | 9 ++- .../targeting/v1/core/QueryBuilder.java | 13 ++++ .../core/OptableAttributesResolverTest.java | 37 ++++++++++++ .../targeting/v1/core/QueryBuilderTest.java | 60 +++++++++++++++++++ 6 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java new file mode 100644 index 00000000000..1b1cdb51c63 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class App { + + String bundle; + + String ver; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index 9dacd6de322..eda2ca74071 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -23,4 +23,6 @@ public class OptableAttributes { String userAgent; Long timeout; + + App app; } 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 8009a9f2ff5..928d2a9f808 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 @@ -8,6 +8,7 @@ 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.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -35,6 +36,7 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() .ips(resolveIp(auctionContext)) .userAgent(resolveUserAgent(auctionContext)) + .app(resolveApp(auctionContext)) .timeout(timeout); if (gdpr != null && gdpr > 0) { @@ -51,7 +53,7 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, } } - if (gppScope.getGppModel() != null) { + if (gppScope != null && gppScope.getGppModel() != null) { builder .gpp(gppScope.getGppModel().encode()) .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); @@ -60,6 +62,11 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, return builder.build(); } + private static App resolveApp(AuctionContext auctionContext) { + final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp(); + return app != null ? App.of(app.getBundle(), app.getVer()) : null; + } + public static String resolveUserAgent(AuctionContext auctionContext) { final Device device = auctionContext.getBidRequest().getDevice(); return device != null ? device.getUa() : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 63f4990f14c..ecd62f7a939 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 @@ -121,6 +121,19 @@ private static String buildAttributesString(OptableAttributes optableAttributes) sb.append("&osdk=").append(REQUEST_SOURCE); + Optional.ofNullable(optableAttributes.getApp()) + .ifPresent(app -> { + final String bundle = app.getBundle(); + if (StringUtils.isNotEmpty(bundle)) { + sb.append("&bundle=").append(bundle); + + final String ver = app.getVer(); + if (StringUtils.isNotEmpty(ver)) { + sb.append("&ver=").append(ver); + } + } + }); + return sb.toString(); } } 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 9621758cab0..38a1cb3520a 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 @@ -11,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; @@ -154,6 +155,42 @@ public void shouldResolveGppAttributes() { .returns(Set.of(1), OptableAttributes::getGppSid); } + @Test + public void shouldResolveAppWhenAppIsPresent() { + // given + final com.iab.openrtb.request.App ortbApp = com.iab.openrtb.request.App.builder() + .bundle("com.example.app") + .ver("1.2.3") + .build(); + final BidRequest bidRequest = BidRequest.builder() + .app(ortbApp) + .build(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(App.of("com.example.app", "1.2.3"), OptableAttributes::getApp); + } + + @Test + public void shouldNotResolveAppWhenAppIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(null, OptableAttributes::getApp); + } + public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { return AuctionContext.builder() .bidRequest(bidRequest) 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 9be6fc3a2b6..25d9992110e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; @@ -250,6 +251,65 @@ public void shouldNotBuildHidWhenHidPrefixesNotConfigured() { assertThat(query.getHid()).isEqualTo(""); } + @Test + public void shouldAppendBundleAndVerWhenAppHasBoth() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).contains("&bundle=com.example.app", "&ver=1.2.3"); + } + + @Test + public void shouldAppendBundleOnlyWhenVerIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).contains("&bundle=com.example.app"); + assertThat(query).doesNotContain("&ver="); + } + + @Test + public void shouldNotAppendBundleAndVerWhenBundleIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).doesNotContain("&bundle=", "&ver="); + } + + @Test + public void shouldNotAppendBundleAndVerWhenAppIsNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).doesNotContain("&bundle=", "&ver="); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) From d2276c1a3dca8db0782e8847b21e12c98a9ae657 Mon Sep 17 00:00:00 2001 From: softcoder Date: Sat, 25 Jul 2026 00:16:25 +0200 Subject: [PATCH 33/56] optable-targeting: Add id5_signature propagation from client to Targeting API --- .../targeting/model/OptableAttributes.java | 2 + .../model/openrtb/ExtUserOptable.java | 2 + .../targeting/v1/core/BidRequestCleaner.java | 2 +- .../v1/core/OptableAttributesResolver.java | 19 ++++ .../targeting/v1/core/QueryBuilder.java | 3 + .../v1/core/BidRequestCleanerTest.java | 4 +- .../core/OptableAttributesResolverTest.java | 102 +++++++++++++----- .../targeting/v1/core/QueryBuilderTest.java | 28 +++++ 8 files changed, 132 insertions(+), 30 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index eda2ca74071..d53d1963795 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -25,4 +25,6 @@ public class OptableAttributes { Long timeout; App app; + + String id5Signature; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java index 787b3adbf1a..a0c8f974b69 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java @@ -17,4 +17,6 @@ public class ExtUserOptable extends FlexibleExtension { String zip; String vid; + + String id5Signature; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java index 30652383942..d467e6df750 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java @@ -14,7 +14,7 @@ public class BidRequestCleaner implements PayloadUpdate { private static final String OPTABLE_FIELD = "optable"; - private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid"); + private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid", "id5_signature"); public static BidRequestCleaner instance() { return new BidRequestCleaner(); 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 928d2a9f808..0122fca451d 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,5 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Regs; @@ -10,6 +12,8 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -59,9 +63,24 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); } + final Optional extUserOptable = Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ext -> ext.getProperty("optable")) + .map(OptableAttributesResolver::parseExtUserOptable); + + extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); + return builder.build(); } + private static ExtUserOptable parseExtUserOptable(JsonNode node) { + try { + return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class); + } catch (JsonProcessingException e) { + return null; + } + } + private static App resolveApp(AuctionContext auctionContext) { final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp(); return app != null ? App.of(app.getBundle(), app.getVer()) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index ecd62f7a939..00aca36e8f8 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 @@ -134,6 +134,9 @@ private static String buildAttributesString(OptableAttributes optableAttributes) } }); + Optional.ofNullable(optableAttributes.getId5Signature()) + .ifPresent(id5Signature -> sb.append("&id5_signature=").append(id5Signature)); + return sb.toString(); } } 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 6aa4ebff3a8..606c89ee102 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 @@ -33,7 +33,8 @@ public void shouldKeepOtherUserExtOptableTags() { // given final User user = givenUser(); ((com.fasterxml.jackson.databind.node.ObjectNode) user.getExt().getProperty("optable")) - .put("other", "value"); + .put("other", "value") + .put("id5_signature", "signature"); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> bidRequest.user(user))); @@ -50,6 +51,7 @@ public void shouldKeepOtherUserExtOptableTags() { .satisfies(optable -> { assertThat(optable.has("other")).isTrue(); assertThat(optable.has("email")).isFalse(); + assertThat(optable.has("id5_signature")).isFalse(); }); } } 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 38a1cb3520a..276c3f30c88 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -1,9 +1,12 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.User; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -84,34 +87,6 @@ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { .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 @@ -191,6 +166,77 @@ public void shouldNotResolveAppWhenAppIsAbsent() { .returns(null, OptableAttributes::getApp); } + @Test + public void shouldResolveId5SignatureWhenPresentInUserExtOptable() { + // given + final BidRequest bidRequest = givenBidRequestWithId5Signature("signature"); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("signature", OptableAttributes::getId5Signature); + } + + @Test + public void shouldNotResolveId5SignatureWhenAbsentInUserExtOptable() { + // given + final BidRequest bidRequest = givenBidRequestWithId5Signature(null); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(null, OptableAttributes::getId5Signature); + } + + private BidRequest givenBidRequestWithId5Signature(String signature) { + final ObjectNode optable = mapper.createObjectNode(); + if (StringUtils.isNotEmpty(signature)) { + optable.set("id5_signature", TextNode.valueOf(signature)); + } + + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", optable); + final User user = User.builder().ext(extUser).build(); + + return BidRequest.builder().user(user).build(); + } + + 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(); + } + public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { return AuctionContext.builder() .bidRequest(bidRequest) 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 25d9992110e..64aff679be4 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 @@ -310,6 +310,34 @@ public void shouldNotAppendBundleAndVerWhenAppIsNull() { assertThat(query).doesNotContain("&bundle=", "&ver="); } + @Test + public void shouldAppendId5SignatureWhenPresent() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .id5Signature("signature") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).contains("&id5_signature=signature"); + } + + @Test + public void shouldNotAppendId5SignatureWhenNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).doesNotContain("&id5_signature="); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) From bfdc8f32b200d1de2e0984497cf5daecc4089ffa Mon Sep 17 00:00:00 2001 From: softcoder Date: Sun, 26 Jul 2026 12:35:44 +0200 Subject: [PATCH 34/56] optable-targeting: Add id5_signature propagation from Targeting API to client --- .../model/openrtb/TargetingResult.java | 3 + .../OptableTargetingAuctionResponseHook.java | 38 ++- ...eTargetingProcessedAuctionRequestHook.java | 2 +- .../targeting/v1/core/Id5Resolver.java | 52 +++++ .../core/Id5SignatureBidResponseEnricher.java | 83 +++++++ .../targeting/v1/net/CachedAPIClient.java | 2 +- .../optable/targeting/v1/BaseOptableTest.java | 32 ++- ...tableTargetingAuctionResponseHookTest.java | 136 ++++++++++- .../optable/targeting/v1/core/CacheTest.java | 3 +- .../targeting/v1/core/Id5ResolverTest.java | 217 ++++++++++++++++++ .../Id5SignatureBidResponseEnricherTest.java | 122 ++++++++++ .../configs/prebid-config-with-optable.yaml | 2 +- 12 files changed, 679 insertions(+), 13 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java index 826ce2698ed..5bc11a92bd3 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; import java.util.List; @@ -10,4 +11,6 @@ public class TargetingResult { List audience; Ortb2 ortb2; + + ObjectNode refs; } 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 8edef6b00eb..a3f0467d7c9 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 @@ -9,10 +9,13 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5SignatureBidResponseEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -62,15 +65,42 @@ public Future> call(AuctionResponsePayl return validationStatus.getStatus() == Status.SUCCESS ? enrichedPayload(moduleContext) - : success(moduleContext); + : enrichedById5SignaturePayload(moduleContext); } private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); - return CollectionUtils.isNotEmpty(targeting) - ? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext) - : success(moduleContext); + return moduleContext.getOptableTargetingCall() + .compose(targetingResult -> + CollectionUtils.isNotEmpty(targeting) + ? update(fullEnrichmentChain(moduleContext, targeting, targetingResult), moduleContext) + : update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) + .recover(throwable -> success(moduleContext)); + } + + private Future> enrichedById5SignaturePayload( + ModuleContext moduleContext) { + + return moduleContext.getOptableTargetingCall() + .compose(targetingResult -> + update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) + .recover(throwable -> success(moduleContext)); + } + + private PayloadUpdate fullEnrichmentChain(ModuleContext moduleContext, + final List targeting, + TargetingResult targetingResult) { + final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); + + return BidResponseEnricher.of(targeting, objectMapper, jsonMerger) + .andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply; + } + + private PayloadUpdate id5SignatureEnrichmentChain(TargetingResult targetingResult) { + final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); + + return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger); } private Future> update( 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 dfa8520f157..384e5c21f1b 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,8 +11,8 @@ 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.TargetingRequestExecutor; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java new file mode 100644 index 00000000000..d7a17c1365a --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -0,0 +1,52 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +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.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class Id5Resolver { + + private Id5Resolver() { + + } + + public static String resolveId5Signature(TargetingResult targetingResult) { + if (targetingResult == null) { + return null; + } + + final String ref = Optional.of(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .map(User::getEids) + .orElseGet(List::of) + .stream() + .filter(it -> "optable.co".equals(it.getInserter()) && "id5-sync.com".equals(it.getSource())) + .map(Eid::getUids) + .flatMap(Collection::stream) + .map(Uid::getExt) + .map(it -> it.at("/optable/ref")) + .filter(Objects::nonNull) + .map(JsonNode::asText) + .findFirst() + .orElse(null); + + if (ref == null) { + return null; + } + + return Optional.ofNullable(targetingResult.getRefs()) + .map(refs -> refs.get(ref)) + .map(it -> it.get("signature")) + .map(JsonNode::asText) + .orElse(null); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java new file mode 100644 index 00000000000..e94af71928d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java @@ -0,0 +1,83 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; + +import java.util.Objects; + +public class Id5SignatureBidResponseEnricher implements PayloadUpdate { + + private final String id5Signature; + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + + private Id5SignatureBidResponseEnricher(String id5Signature, ObjectMapper mapper, JsonMerger jsonMerger) { + this.id5Signature = id5Signature; + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public static Id5SignatureBidResponseEnricher of(String id5Signature, ObjectMapper mapper, JsonMerger jsonMerger) { + return new Id5SignatureBidResponseEnricher(id5Signature, mapper, jsonMerger); + } + + @Override + public AuctionResponsePayload apply(AuctionResponsePayload payload) { + return AuctionResponsePayloadImpl.of(enrichBidResponse(payload.bidResponse(), id5Signature)); + } + + private BidResponse enrichBidResponse(BidResponse bidResponse, String id5Signature) { + if (StringUtils.isEmpty(id5Signature)) { + return bidResponse; + } + final ObjectNode passthroughNode = id5SignatureToObjectNode(id5Signature); + + final ExtBidResponse existingExt = bidResponse.getExt(); + final ExtBidResponsePrebid existingPrebid = existingExt != null ? existingExt.getPrebid() : null; + final JsonNode existingPassthrough = existingPrebid != null ? existingPrebid.getPassthrough() : null; + + final JsonNode mergedPassthrough = existingPassthrough != null + ? jsonMerger.merge(passthroughNode, existingPassthrough) + : passthroughNode; + + final ExtBidResponsePrebid modifiedPrebid = existingPrebid != null + ? existingPrebid.toBuilder() + .passthrough(mergedPassthrough) + .build() + : ExtBidResponsePrebid.builder() + .passthrough(mergedPassthrough) + .build(); + + final ExtBidResponse modifiedExt = existingExt != null + ? existingExt.toBuilder() + .prebid(modifiedPrebid) + .build() + : ExtBidResponse.builder() + .prebid(modifiedPrebid) + .build(); + + return bidResponse.toBuilder() + .ext(modifiedExt) + .build(); + } + + private ObjectNode id5SignatureToObjectNode(String id5Signature) { + final ObjectNode node = mapper.createObjectNode(); + node.set("id5_signature", TextNode.valueOf(id5Signature)); + + final ObjectNode optableNode = mapper.createObjectNode(); + optableNode.set("optable", node); + + return optableNode; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index e7e8bc3e452..eabccfdbff0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -42,7 +42,7 @@ public Future getTargeting(OptableTargetingProperties propertie return cache.get(createCachingKey(tenant, origin, ips, query, true)) .recover(ignore -> apiClient.getTargeting(properties, query, ips, userAgent, timeout) .recover(throwable -> isCircuitBreakerEnabled - ? Future.succeededFuture(new TargetingResult(null, null)) + ? Future.succeededFuture(new TargetingResult(null, null, null)) : Future.failedFuture(throwable)) .compose(result -> cache.put( createCachingKey(tenant, origin, ips, query, false), 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 4bfb1947b5f..2ebf65c7b6e 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 @@ -17,6 +17,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import org.apache.commons.io.IOUtils; @@ -73,6 +74,28 @@ protected ModuleContext givenModuleContext(List audiences) { return moduleContext; } + protected ModuleContext givenModuleContext(List audiences, Future optableTargetingCall) { + final ModuleContext moduleContext = givenModuleContext(audiences); + moduleContext.setOptableTargetingCall(optableTargetingCall); + return moduleContext; + } + + protected Eid givenId5Eid(String refValue) { + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode().set("ref", TextNode.valueOf(refValue))); + return Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + } + + protected ObjectNode givenRefsObject(String refValue, String signature) { + final ObjectNode refs = mapper.createObjectNode(); + refs.set(refValue, mapper.createObjectNode().set("signature", TextNode.valueOf(signature))); + return refs; + } + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout, Account account) { @@ -168,17 +191,22 @@ protected TargetingResult givenTargetingResult() { } protected TargetingResult givenTargetingResult(List eids, List data) { + return givenTargetingResult(eids, data, null); + } + + protected TargetingResult givenTargetingResult(List eids, List data, ObjectNode refs) { return new TargetingResult( List.of(new Audience( "provider", List.of(new AudienceId("id")), "keyspace", 1)), - new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data))); + new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data)), + refs); } protected TargetingResult givenEmptyTargetingResult() { - return new TargetingResult(Collections.emptyList(), new Ortb2(null)); + return new TargetingResult(Collections.emptyList(), new Ortb2(null), null); } protected User givenUser() { 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 7d1df51bc5e..29429910f8d 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -1,6 +1,8 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; @@ -12,6 +14,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -60,7 +63,8 @@ public void shouldHaveCode() { @Test public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { // given - when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); // when final Future> future = @@ -93,7 +97,8 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)))); + 1)), + Future.succeededFuture(givenEmptyTargetingResult()))); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -126,6 +131,130 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); } + @Test + public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + when(invocationContext.moduleContext()).thenReturn(givenModuleContext( + List.of(new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.succeededFuture(targetingResult))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + + @Test + public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.succeededFuture(targetingResult))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + + @Test + public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsPresent() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext( + List.of(new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.failedFuture(new RuntimeException("error")))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // 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); + } + + @Test + public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + // given + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); + + // 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); + } + @Test public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { // given @@ -134,7 +263,8 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)))); + 1)), + Future.failedFuture(new RuntimeException("error")))); // when final Future> future = diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java index 1917fd6ca27..e0a723bbfa3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -109,6 +109,7 @@ private TargetingResult givenTargetingResult() { List.of(new AudienceId("1")), "keyspace", 0)), - new Ortb2(new User(null, null))); + new Ortb2(new User(null, null)), + null); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java new file mode 100644 index 00000000000..b0b9be37b21 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -0,0 +1,217 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import org.junit.jupiter.api.Test; +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.json.ObjectMapperProvider; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Id5ResolverTest { + + @Test + public void shouldReturnNullWhenTargetingResultIsNull() { + // when + final String result = Id5Resolver.resolveId5Signature(null); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenOrtb2IsNull() { + // given + final TargetingResult targetingResult = new TargetingResult(List.of(), null, null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenUserHasNoEids() { + // given + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(null, null)), + null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenEidsDoNotMatchOptableAndId5Source() { + // given + final Eid eid = Eid.builder() + .source("other-source") + .inserter("other-inserter") + .uids(List.of(Uid.builder().id("id").build())) + .build(); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("otherRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("signatureValue"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnSignatureWhenAllConditionsAreMet() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("signatureValue"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isEqualTo("signatureValue"); + } + + @Test + public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { + // given + final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); + firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("firstRef"))); + + final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); + secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("secondRef"))); + + final Eid firstEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id1").ext(firstUidExt).build())) + .build(); + final Eid secondEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("firstRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("firstSignature"))); + refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("secondSignature"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(firstEid, secondEid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isEqualTo("firstSignature"); + } + + @Test + public void shouldReturnNullWhenRefEntryHasNoSignature() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("other", TextNode.valueOf("value"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java new file mode 100644 index 00000000000..d9eb8bd1798 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java @@ -0,0 +1,122 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.response.BidResponse; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Id5SignatureBidResponseEnricherTest { + + private final JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper()); + private final JsonMerger jsonMerger = new JsonMerger(jacksonMapper); + + @Test + public void shouldReturnOriginBidResponseWhenId5SignatureIsNull() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of(null, ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + assertThat(result.bidResponse()).isSameAs(bidResponse); + } + + @Test + public void shouldReturnOriginBidResponseWhenId5SignatureIsEmpty() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + assertThat(result.bidResponse()).isSameAs(bidResponse); + } + + @Test + public void shouldAddId5SignatureToPassthroughWhenExtIsAbsent() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + final BidResponse enriched = result.bidResponse(); + assertThat(enriched.getExt()).isNotNull(); + assertThat(enriched.getExt().getPrebid()).isNotNull(); + final JsonNode passthrough = enriched.getExt().getPrebid().getPassthrough(); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo("signature"); + } + + @Test + public void shouldMergeId5SignatureWithExistingPassthrough() { + // given + final ObjectNode existingPassthrough = ObjectMapperProvider.mapper().createObjectNode(); + existingPassthrough.set("other", ObjectMapperProvider.mapper().createObjectNode() + .set("value", TextNode.valueOf("otherValue"))); + existingPassthrough.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("existing", TextNode.valueOf("preserved"))); + + final ExtBidResponse ext = ExtBidResponse.builder() + .prebid(ExtBidResponsePrebid.builder() + .passthrough(existingPassthrough) + .build()) + .build(); + final BidResponse bidResponse = BidResponse.builder().ext(ext).build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + final JsonNode passthrough = result.bidResponse().getExt().getPrebid().getPassthrough(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo("signature"); + assertThat(passthrough.get("optable").get("existing").asText()).isEqualTo("preserved"); + assertThat(passthrough.get("other").get("value").asText()).isEqualTo("otherValue"); + } + + @Test + public void shouldPreserveExistingPrebidFieldsWhenAddingPassthrough() { + // given + final ExtBidResponse ext = ExtBidResponse.builder() + .prebid(ExtBidResponsePrebid.builder().build()) + .build(); + final BidResponse bidResponse = BidResponse.builder().ext(ext).build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + final ExtBidResponsePrebid prebid = result.bidResponse().getExt().getPrebid(); + assertThat(prebid).isNotNull(); + assertThat(prebid.getPassthrough().get("optable").get("id5_signature").asText()) + .isEqualTo("signature"); + } +} diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index 77514babf26..ae85e0ad69f 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://na.edge.optable.co/v2/targeting?t={TENANT}&o={ORIGIN} From 4b456ad75cf950d36633b6f59e283008c46f3ce3 Mon Sep 17 00:00:00 2001 From: softcoder Date: Sun, 26 Jul 2026 16:40:37 +0200 Subject: [PATCH 35/56] optable-targeting: code cleanup --- .../optable/targeting/v1/core/Id5Resolver.java | 15 ++++++++++++--- .../v1/core/OptableAttributesResolver.java | 4 +++- .../optable/targeting/v1/core/QueryBuilder.java | 6 ++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index d7a17c1365a..e9ba337fb3b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -14,6 +14,12 @@ public class Id5Resolver { + public static final String STR_OPTABLE_CO = "optable.co"; + public static final String STR_ID_5_SYNC_COM = "id5-sync.com"; + public static final String STR_OPTABLE = "optable"; + public static final String STR_REF = "ref"; + public static final String STR_SIGNATURE = "signature"; + private Id5Resolver() { } @@ -29,11 +35,14 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .map(User::getEids) .orElseGet(List::of) .stream() - .filter(it -> "optable.co".equals(it.getInserter()) && "id5-sync.com".equals(it.getSource())) + .filter(it -> STR_OPTABLE_CO.equals(it.getInserter()) && STR_ID_5_SYNC_COM.equals(it.getSource())) .map(Eid::getUids) .flatMap(Collection::stream) .map(Uid::getExt) - .map(it -> it.at("/optable/ref")) + .filter(Objects::nonNull) + .map(it -> it.get(STR_OPTABLE)) + .filter(Objects::nonNull) + .map(it -> it.get(STR_REF)) .filter(Objects::nonNull) .map(JsonNode::asText) .findFirst() @@ -45,7 +54,7 @@ public static String resolveId5Signature(TargetingResult targetingResult) { return Optional.ofNullable(targetingResult.getRefs()) .map(refs -> refs.get(ref)) - .map(it -> it.get("signature")) + .map(it -> it.get(STR_SIGNATURE)) .map(JsonNode::asText) .orElse(null); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 0122fca451d..b655f04cb61 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 @@ -68,7 +68,9 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .map(ext -> ext.getProperty("optable")) .map(OptableAttributesResolver::parseExtUserOptable); - extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); + if (extUserOptable.isPresent()) { + extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); + } return builder.build(); } 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 00aca36e8f8..9e6c0da251a 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 @@ -49,7 +49,8 @@ private static String buildHidString(List ids, String hidPrefixesString) { if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(hidPrefixesString)) { return StringUtils.EMPTY; } - final Map prefixToIdValue = ids.stream().collect(Collectors.toMap(Id::getName, it -> it)); + final Map prefixToIdValue = ids.stream() + .collect(Collectors.toMap(Id::getName, it -> it, (a, b) -> b)); if (MapUtils.isEmpty(prefixToIdValue)) { return StringUtils.EMPTY; } @@ -135,7 +136,8 @@ private static String buildAttributesString(OptableAttributes optableAttributes) }); Optional.ofNullable(optableAttributes.getId5Signature()) - .ifPresent(id5Signature -> sb.append("&id5_signature=").append(id5Signature)); + .ifPresent(id5Signature -> sb.append("&id5_signature=") + .append(URLEncoder.encode(id5Signature, StandardCharsets.UTF_8))); return sb.toString(); } From 98a985395b0dd025691e259a9abaf2a8508516c0 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 19:37:18 +0200 Subject: [PATCH 36/56] optable-targeting: fix NPE when optableTargetingCall was never set at resolving id5Signature --- .../targeting/model/ModuleContext.java | 2 + .../v1/OptableBidderRequestHook.java | 2 + .../OptableTargetingAuctionResponseHook.java | 26 ++++------- ...eTargetingProcessedAuctionRequestHook.java | 2 + .../v1/OptableBidderRequestHookTest.java | 46 +++++++++++++++++++ ...tableTargetingAuctionResponseHookTest.java | 43 +++++++++-------- ...getingProcessedAuctionRequestHookTest.java | 44 ++++++++++++++++++ 7 files changed, 129 insertions(+), 36 deletions(-) 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 5574a7a72d8..ac7e30eec8d 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 @@ -35,6 +35,8 @@ public class ModuleContext { private boolean shouldSkipEnrichment; + private String id5Signature; + 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/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index b5ce4cd7531..f96335cb91d 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 @@ -10,6 +10,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.BidderRequestEnricher; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -58,6 +59,7 @@ private Future> enrichedPayload(Targeting if (hasData) { moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); } 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 a3f0467d7c9..1f98c18cc0d 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 @@ -9,12 +9,10 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5SignatureBidResponseEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -70,36 +68,32 @@ public Future> call(AuctionResponsePayl private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); + final String id5Signature = moduleContext.getId5Signature(); - return moduleContext.getOptableTargetingCall() - .compose(targetingResult -> - CollectionUtils.isNotEmpty(targeting) - ? update(fullEnrichmentChain(moduleContext, targeting, targetingResult), moduleContext) - : update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) - .recover(throwable -> success(moduleContext)); + return CollectionUtils.isNotEmpty(targeting) + ? update(fullEnrichmentChain(targeting, id5Signature), moduleContext) + : update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } private Future> enrichedById5SignaturePayload( ModuleContext moduleContext) { + final String id5Signature = moduleContext.getId5Signature(); + return moduleContext.getOptableTargetingCall() .compose(targetingResult -> - update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) + update(id5SignatureEnrichmentChain(id5Signature), moduleContext)) .recover(throwable -> success(moduleContext)); } - private PayloadUpdate fullEnrichmentChain(ModuleContext moduleContext, - final List targeting, - TargetingResult targetingResult) { - final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); + private PayloadUpdate fullEnrichmentChain(final List targeting, + final String id5Signature) { return BidResponseEnricher.of(targeting, objectMapper, jsonMerger) .andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply; } - private PayloadUpdate id5SignatureEnrichmentChain(TargetingResult targetingResult) { - final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); - + private PayloadUpdate id5SignatureEnrichmentChain(String id5Signature) { return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger); } 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 384e5c21f1b..fc22fdc354c 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.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; import org.prebid.server.hooks.v1.InvocationAction; @@ -105,6 +106,7 @@ private Future> enrichPayload( OptableTargetingProperties properties) { moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); final PayloadUpdate payloadUpdate = 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 2bcce42ac94..4180f8645cc 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,7 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,6 +16,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.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; 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; @@ -23,6 +26,7 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeoutException; @@ -170,6 +174,48 @@ public void shouldUpdateModuleContextWithTargetingOnSuccess() { .isEqualTo("success"); } + @Test + public void shouldSetId5SignatureOnModuleContextWhenTargetingResultHasId5Signature() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100); + moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); + + // when + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + } + + @Test + public void shouldNotSetId5SignatureOnModuleContextWhenTargetingResultHasNoId5Signature() { + // given + 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"); + + // when + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(moduleContext.getId5Signature()).isNull(); + } + @Test public void shouldReturnNoActionWithNoDataOutcomeWhenTargetingResultHasNoUser() { // given 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 29429910f8d..5b37eeec80f 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 @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Eid; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; @@ -14,7 +13,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -134,19 +132,15 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( @Test public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { // given - final String refValue = "refValue"; final String signature = "id5Signature"; - final Eid id5Eid = givenId5Eid(refValue); - final ObjectNode refs = givenRefsObject(refValue, signature); - final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); - - when(invocationContext.moduleContext()).thenReturn(givenModuleContext( - List.of(new Audience( + final ModuleContext moduleContext = givenModuleContext(List.of( + new Audience( "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)), - Future.succeededFuture(targetingResult))); + 1))); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -181,14 +175,11 @@ public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { @Test public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() { // given - final String refValue = "refValue"; final String signature = "id5Signature"; - final Eid id5Eid = givenId5Eid(refValue); - final ObjectNode refs = givenRefsObject(refValue, signature); - final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); - - when(invocationContext.moduleContext()).thenReturn( - givenModuleContext(null, Future.succeededFuture(targetingResult))); + final ModuleContext moduleContext = + givenModuleContext(null, Future.succeededFuture(givenEmptyTargetingResult())); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -212,7 +203,7 @@ public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() { } @Test - public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsPresent() { + public void shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargetingCallFails() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext( List.of(new Audience( @@ -227,13 +218,25 @@ public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsPrese final Future> future = target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); } @Test 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 894c8082175..57f88b596b0 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 @@ -156,6 +156,50 @@ void callShouldReturnResultWithUpdateActionWhenOptableTargetingReturnsTargeting( .containsExactly("id"); } + @Test + void callShouldSetId5SignatureOnModuleContextWhenTargetingResultContainsId5Signature() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult(List.of(id5Eid), null, refs))); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat((ModuleContext) future.result().moduleContext()) + .isNotNull() + .extracting(ModuleContext::getId5Signature) + .isEqualTo(signature); + } + + @Test + void callShouldLeaveId5SignatureNullWhenTargetingResultHasNoId5Signature() { + // given + 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(); + assertThat((ModuleContext) future.result().moduleContext()) + .isNotNull() + .extracting(ModuleContext::getId5Signature) + .isNull(); + } + @Test void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { // given From df5c88547d2528719ae902fa7a99bf4a6e406996 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:04:55 +0200 Subject: [PATCH 37/56] optable-targeting: Encode Targeting API query params --- .../targeting/v1/core/QueryBuilder.java | 7 +++-- .../targeting/v1/core/QueryBuilderTest.java | 30 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 9e6c0da251a..ed3c5905222 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 @@ -58,7 +58,8 @@ private static String buildHidString(List ids, String hidPrefixesString) { final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) .map(prefixToIdValue::get) .filter(Objects::nonNull) - .map(it -> String.format("hid=%s:%s", it.getName(), it.getValue())) + .map(it -> String.format( + "hid=%s:%s", it.getName(), URLEncoder.encode(it.getValue(), StandardCharsets.UTF_8))) .collect(Collectors.joining("&")); if (StringUtils.isEmpty(hidParameters)) { return StringUtils.EMPTY; @@ -126,11 +127,11 @@ private static String buildAttributesString(OptableAttributes optableAttributes) .ifPresent(app -> { final String bundle = app.getBundle(); if (StringUtils.isNotEmpty(bundle)) { - sb.append("&bundle=").append(bundle); + sb.append("&bundle=").append(URLEncoder.encode(bundle, StandardCharsets.UTF_8)); final String ver = app.getVer(); if (StringUtils.isNotEmpty(ver)) { - sb.append("&ver=").append(ver); + sb.append("&ver=").append(URLEncoder.encode(ver, StandardCharsets.UTF_8)); } } }); 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 64aff679be4..08f6ce82815 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 @@ -207,7 +207,7 @@ public void shouldBuildHidWhenHidPrefixesMatchIds() { final Query query = QueryBuilder.build(ids, optableAttributes, props); // then - assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0:0:0:0:0:0:0:1"); + assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0%3A0%3A0%3A0%3A0%3A0%3A0%3A1"); } @Test @@ -223,7 +223,33 @@ public void shouldExcludeDeviceIpV6FromIdsString() { // then assertThat(query.getIds()).doesNotContain(Id.DEVICE_IP_V_6); - assertThat(query.getHid()).isEqualTo("&hid=i6:0:0:0:0:0:0:0:1"); + assertThat(query.getHid()).isEqualTo("&hid=i6:0%3A0%3A0%3A0%3A0%3A0%3A0%3A1"); + } + + @Test + public void shouldUrlEncodeHidValueWithSpecialCharacters() { + // given + final OptableTargetingProperties props = givenProperties(null, "c"); + final List ids = List.of(Id.of("c", "a b&c=d+e/f")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo("&hid=c:a+b%26c%3Dd%2Be%2Ff"); + } + + @Test + public void shouldLeaveHidValueUnchangedForAlphanumericValue() { + // given + final OptableTargetingProperties props = givenProperties(null, "c"); + final List ids = List.of(Id.of("c", "abc123")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo("&hid=c:abc123"); } @Test From 66eef5225def88c86ff6aee6639f025157e4324e Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:16:40 +0200 Subject: [PATCH 38/56] optable-targeting: improve hid prefixes parsing --- .../hooks/modules/optable/targeting/v1/core/QueryBuilder.java | 2 ++ 1 file changed, 2 insertions(+) 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 ed3c5905222..0385ff294f2 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 @@ -56,6 +56,8 @@ private static String buildHidString(List ids, String hidPrefixesString) { } final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) .map(prefixToIdValue::get) .filter(Objects::nonNull) .map(it -> String.format( From a2db76cf96804e900fd19d32b8032c4db2e171f1 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:17:30 +0200 Subject: [PATCH 39/56] optable-targeting: Remove dead code --- .../hooks/modules/optable/targeting/v1/core/QueryBuilder.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 0385ff294f2..66391ee4634 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 @@ -51,9 +51,6 @@ private static String buildHidString(List ids, String hidPrefixesString) { } final Map prefixToIdValue = ids.stream() .collect(Collectors.toMap(Id::getName, it -> it, (a, b) -> b)); - if (MapUtils.isEmpty(prefixToIdValue)) { - return StringUtils.EMPTY; - } final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) .map(String::trim) From 0e0cfb6733a6feb4f4e4b995d1bd7142a943401c Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:48:09 +0200 Subject: [PATCH 40/56] optable-targeting: Improve id5 signature extraction from Targeting response --- .../targeting/v1/core/Id5Resolver.java | 25 ++++-- .../targeting/v1/core/QueryBuilder.java | 1 - .../targeting/v1/core/Id5ResolverTest.java | 90 +++++++++++++++++++ 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index e9ba337fb3b..908520b5f6f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -1,8 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Eid; 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.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; @@ -29,7 +32,7 @@ public static String resolveId5Signature(TargetingResult targetingResult) { return null; } - final String ref = Optional.of(targetingResult) + final List refs = Optional.of(targetingResult) .map(TargetingResult::getOrtb2) .map(Ortb2::getUser) .map(User::getEids) @@ -37,6 +40,7 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .stream() .filter(it -> STR_OPTABLE_CO.equals(it.getInserter()) && STR_ID_5_SYNC_COM.equals(it.getSource())) .map(Eid::getUids) + .filter(Objects::nonNull) .flatMap(Collection::stream) .map(Uid::getExt) .filter(Objects::nonNull) @@ -45,17 +49,26 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .map(it -> it.get(STR_REF)) .filter(Objects::nonNull) .map(JsonNode::asText) - .findFirst() - .orElse(null); + .toList(); - if (ref == null) { + if (CollectionUtils.isEmpty(refs)) { return null; } - return Optional.ofNullable(targetingResult.getRefs()) - .map(refs -> refs.get(ref)) + final ObjectNode references = targetingResult.getRefs(); + if (references == null) { + return null; + } + + return refs.stream() + .map(references::get) + .filter(Objects::nonNull) .map(it -> it.get(STR_SIGNATURE)) + .filter(Objects::nonNull) + .filter(JsonNode::isValueNode) .map(JsonNode::asText) + .filter(StringUtils::isNotBlank) + .findFirst() .orElse(null); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 66391ee4634..ad3ba1efefd 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -1,7 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java index b0b9be37b21..c38a0821d49 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -188,6 +188,96 @@ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { assertThat(result).isEqualTo("firstSignature"); } + @Test + public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { + // given + final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); + firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("firstRef"))); + + final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); + secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("secondRef"))); + + final Eid firstEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id1").ext(firstUidExt).build())) + .build(); + final Eid secondEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("secondSignature"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(firstEid, secondEid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isEqualTo("secondSignature"); + } + + @Test + public void shouldReturnNullWhenRefEntrySignatureIsBlank() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf(" "))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", ObjectMapperProvider.mapper().createObjectNode())); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + @Test public void shouldReturnNullWhenRefEntryHasNoSignature() { // given From e67c08cbfafc5e9736fb977acc2378f2e4cd618f Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 22:08:49 +0200 Subject: [PATCH 41/56] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 7 +- .../v1/core/ExtUserOptableResolver.java | 26 ++++++++ .../targeting/v1/core/Id5Resolver.java | 1 - .../optable/targeting/v1/core/IdsMapper.java | 18 +----- .../v1/core/OptableAttributesResolver.java | 26 +++----- .../v1/core/TargetingRequestExecutor.java | 10 ++- .../v1/OptableRawAuctionRequestHookTest.java | 3 +- ...getingProcessedAuctionRequestHookTest.java | 3 +- .../v1/core/ExtUserOptableResolverTest.java | 64 +++++++++++++++++++ .../core/OptableAttributesResolverTest.java | 16 ++--- .../configs/sample-app-settings-optable.yaml | 2 +- 11 files changed, 123 insertions(+), 53 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.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 cc05cfe2a02..91ae6691bbf 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 @@ -98,10 +98,11 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean TargetingRequestExecutor targetingRequestExecutor(OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - TimeoutFactory timeoutFactory) { + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { - return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); + return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory, logSamplingRate); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java new file mode 100644 index 00000000000..1d7e081148e --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; + +public class ExtUserOptableResolver { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(ExtUserOptableResolver.class)); + + private ExtUserOptableResolver() { + } + + public static ExtUserOptable resolveExtUserOptable(JsonNode node, double logSamplingRate) { + try { + return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class); + } catch (JsonProcessingException e) { + conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate); + return null; + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index 908520b5f6f..564d2c4a142 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -24,7 +24,6 @@ public class Id5Resolver { public static final String STR_SIGNATURE = "signature"; private Id5Resolver() { - } public static String resolveId5Signature(TargetingResult targetingResult) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index a9b4c9c7311..ddb47141248 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -1,7 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -14,8 +12,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OS; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.LoggerFactory; import java.util.Collections; import java.util.HashMap; @@ -26,9 +22,6 @@ public class IdsMapper { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(IdsMapper.class)); - private static final Map STATIC_PPID_MAPPING = Map.of( "id5-sync.com", Id.ID5, "utiq.com", Id.UTIQ, @@ -60,7 +53,7 @@ private void addOptableIds(Map ids, User user) { final Optional extUserOptable = Optional.ofNullable(user) .map(User::getExt) .map(ext -> ext.getProperty("optable")) - .map(this::parseExtUserOptable); + .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate)); extUserOptable.map(ExtUserOptable::getEmail).ifPresent(it -> ids.put(Id.EMAIL, it)); extUserOptable.map(ExtUserOptable::getPhone).ifPresent(it -> ids.put(Id.PHONE, it)); @@ -68,15 +61,6 @@ private void addOptableIds(Map ids, User user) { extUserOptable.map(ExtUserOptable::getVid).ifPresent(it -> ids.put(Id.OPTABLE_VID, it)); } - private ExtUserOptable parseExtUserOptable(JsonNode node) { - try { - return objectMapper.treeToValue(node, ExtUserOptable.class); - } catch (JsonProcessingException e) { - conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate); - return null; - } - } - private static void addDeviceIds(Map ids, Device device) { final String ifa = device != null ? device.getIfa() : null; final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null; 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 b655f04cb61..39783e9bf34 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,7 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Regs; @@ -13,7 +11,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; -import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -26,7 +23,10 @@ public class OptableAttributesResolver { private OptableAttributesResolver() { } - public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { + public static OptableAttributes resolveAttributes(AuctionContext auctionContext, + Long timeout, + double logSamplingRate) { + final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); final BidRequest bidRequest = auctionContext.getBidRequest(); @@ -63,26 +63,16 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); } - final Optional extUserOptable = Optional.ofNullable(bidRequest.getUser()) + Optional.ofNullable(bidRequest.getUser()) .map(User::getExt) .map(ext -> ext.getProperty("optable")) - .map(OptableAttributesResolver::parseExtUserOptable); - - if (extUserOptable.isPresent()) { - extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); - } + .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate)) + .map(ExtUserOptable::getId5Signature) + .ifPresent(builder::id5Signature); return builder.build(); } - private static ExtUserOptable parseExtUserOptable(JsonNode node) { - try { - return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class); - } catch (JsonProcessingException e) { - return null; - } - } - private static App resolveApp(AuctionContext auctionContext) { final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp(); return app != null ? App.of(app.getBundle(), app.getVer()) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 36cd33c7724..709b18979f5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java @@ -29,14 +29,17 @@ public class TargetingRequestExecutor { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; private final TimeoutFactory timeoutFactory; + private double logSamplingRate; public TargetingRequestExecutor(OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - TimeoutFactory timeoutFactory) { + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory, + double logSamplingRate) { this.optableTargeting = Objects.requireNonNull(optableTargeting); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); this.timeoutFactory = ObjectUtils.requireNonEmpty(timeoutFactory); + this.logSamplingRate = logSamplingRate; } public Future makeRequest(AuctionRequestPayload payload, @@ -51,7 +54,8 @@ public Future makeRequest(AuctionRequestPayload payload, : timeoutFactory.create(getHookTimeout(invocationContext).remaining() + apiTimeout); final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), - properties.getTimeout()); + properties.getTimeout(), + logSamplingRate); return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout); } 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 059db74ce0e..4e1a5b3e5f1 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 @@ -63,7 +63,8 @@ public void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - targetingRequestExecutor = new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); + targetingRequestExecutor = new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, 0.01); target = new OptableRawAuctionRequestHook( configResolver, targetingRequestExecutor, bidderEnrichmentSampler, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); 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 57f88b596b0..c804be0b508 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 @@ -80,7 +80,8 @@ void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - targetingRequestExecutor = new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); + targetingRequestExecutor = new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, 0.01); target = new OptableTargetingProcessedAuctionRequestHook( configResolver, targetingRequestExecutor, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java new file mode 100644 index 00000000000..b3142c173f2 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java @@ -0,0 +1,64 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExtUserOptableResolverTest { + + @Test + public void shouldResolveExtUserOptableWhenNodeIsValid() { + // given + final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode(); + node.set("email", TextNode.valueOf("user@example.com")); + node.set("phone", TextNode.valueOf("123")); + node.set("zip", TextNode.valueOf("321")); + node.set("vid", TextNode.valueOf("vid")); + node.set("id5_signature", TextNode.valueOf("signature")); + + // when + final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0); + + // then + assertThat(result).isNotNull() + .returns("user@example.com", ExtUserOptable::getEmail) + .returns("123", ExtUserOptable::getPhone) + .returns("321", ExtUserOptable::getZip) + .returns("vid", ExtUserOptable::getVid) + .returns("signature", ExtUserOptable::getId5Signature); + } + + @Test + public void shouldReturnNullWhenNodeIsNotParseable() { + // given + final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode(); + node.set("email", ObjectMapperProvider.mapper().createArrayNode().add(1).add(2)); + + // when + final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldResolveExtUserOptableWhenNodeIsEmpty() { + // given + final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode(); + + // when + final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0); + + // then + assertThat(result).isNotNull() + .returns(null, ExtUserOptable::getEmail) + .returns(null, ExtUserOptable::getPhone) + .returns(null, ExtUserOptable::getZip) + .returns(null, ExtUserOptable::getVid) + .returns(null, ExtUserOptable::getId5Signature); + } +} 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 276c3f30c88..51adfda9952 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 @@ -60,7 +60,7 @@ public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -79,7 +79,7 @@ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -100,7 +100,7 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -121,7 +121,7 @@ public void shouldResolveGppAttributes() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -144,7 +144,7 @@ public void shouldResolveAppWhenAppIsPresent() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -159,7 +159,7 @@ public void shouldNotResolveAppWhenAppIsAbsent() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -174,7 +174,7 @@ public void shouldResolveId5SignatureWhenPresentInUserExtOptable() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -189,7 +189,7 @@ public void shouldNotResolveId5SignatureWhenAbsentInUserExtOptable() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 7b4af19a7e2..b05448d65ca 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -31,7 +31,7 @@ accounts: enrich-app: true id-prefix-order: "e,v,c" hid-prefixes: "c,i6" - ppid-mapping: { "pubmatic.com": "c" } + ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: enabled: false From e13add0bfc0e7c3fda9e2f57851294d943ad6bd4 Mon Sep 17 00:00:00 2001 From: softcoder Date: Tue, 28 Jul 2026 09:08:09 +0200 Subject: [PATCH 42/56] optable-targeting: update README.md --- extra/modules/optable-targeting/README.md | 51 ++++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 9e3b76c6446..901ad2fa40f 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -203,6 +203,7 @@ would result in this nesting in the JSON configuration: | 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 | +| hid-prefixes | no | string | none | An optional string of comma separated id prefixes that should additionally be sent to the Targeting API as resolver hints in `hid=prefix:value` query parameters. See the section on Resolver Hints (hid) below for more detail. | | 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. 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). | @@ -239,8 +240,8 @@ on identifier types. Targeting API accepts multiple id parameters - and their or ### Optable input erasure -**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields will be removed by the module from the original -OpenRTB request before being sent to bidders. +**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` and `.id5_signature` fields will be removed by the module +from the original OpenRTB request before being sent to bidders. ### Publisher Provided IDs (PPID) Mapping @@ -263,6 +264,52 @@ ppid-mapping: {"id5-sync.com": "c1"} This will lead to id5 ID supplied as `id=c1:...` to the Targeting API. +### Resolver Hints (`hid`) + +In addition to the regular `id=prefix:value` parameters, the module can forward selected identifiers to the Targeting +API as resolver hints using the `hid=prefix:value` query parameter form. The set of prefixes that should be sent as +`hid` is configured via the `hid-prefixes` parameter, which accepts a comma-separated list of prefix names, f.e.: + +```yaml +hid-prefixes: "c, i6" +``` + +## Targeting API Query Attributes + +In addition to the identifier parameters, the module forwards the following attributes as query string parameters to +the Targeting API: + +| Attribute | Source | +|------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `gdpr` | `1` if GDPR applies to the request, `0` otherwise. | +| `gdpr_consent` | TCF consent string, sent when available and the consent is valid. | +| `gpp` | GPP string, sent when available in the resolved GPP context. | +| `gpp_sid` | Comma-separated list of active GPP section IDs (limited to the first two), sent when the set is non-empty. | +| `timeout` | Soft timeout hint (in ms, suffixed with `ms`), sent when the module-level `timeout` property is configured. | +| `osdk` | Always set to `prebid-server`, identifying the caller. | +| `bundle` | App bundle identifier, URL-encoded. Sent when the incoming request has an `app` object (`request.app.bundle`) and the bundle is non-empty. | +| `ver` | App version, URL-encoded. Sent only when `bundle` is non-empty and `request.app.ver` is present and non-empty. | +| `id5_signature` | ID5 signature, URL-encoded. Sent when the resolved `user.ext.optable.id5_signature` is present in the incoming request | + +### App bundle and version + +For app traffic (requests with an `app` object) the module forwards the application's bundle identifier as the +`bundle=` query parameter and, when available, its version as `ver=`. Both values are URL-encoded. The `ver` parameter +is only sent when `bundle` is present and non-empty — requests with a version but no bundle will not produce either +parameter. + +### ID5 signature + +The ID5 signature is propagated to the Targeting API in two ways: + +1. **Request-side signature** — when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value + is sent to the Targeting API as the `id5_signature=` query parameter. +2. **Response-side signature** — when the Targeting API response contains refs with an ID5 signature, the module + resolves the signature through the matching optable-eid and propagates it back into the request context, where it + is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use. + +The `id5_signature` field is also part of the Optable input erasure — see below. + ## Analytics Tags The following 2 analytics tags are written by the module: From 379a73ab1345976136a4e1ae8889dd1d38f88649 Mon Sep 17 00:00:00 2001 From: softcoder Date: Tue, 28 Jul 2026 16:52:26 +0200 Subject: [PATCH 43/56] optable-targeting: Code cleanup --- extra/modules/optable-targeting/README.md | 2 +- .../v1/OptableBidderRequestHook.java | 23 +++++- .../OptableTargetingAuctionResponseHook.java | 14 +--- .../targeting/v1/core/Id5Resolver.java | 19 ++--- .../targeting/v1/core/QueryBuilder.java | 4 +- .../v1/core/TargetingRequestExecutor.java | 2 +- .../v1/OptableBidderRequestHookTest.java | 55 +++++++++++++ ...tableTargetingAuctionResponseHookTest.java | 25 ++++-- .../targeting/v1/core/Id5ResolverTest.java | 81 ++++++++++--------- 9 files changed, 155 insertions(+), 70 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 901ad2fa40f..1dae9829156 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -308,7 +308,7 @@ The ID5 signature is propagated to the Targeting API in two ways: resolves the signature through the matching optable-eid and propagates it back into the request context, where it is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use. -The `id5_signature` field is also part of the Optable input erasure — see below. +The `id5_signature` field is also part of the Optable input erasure — see above. ## Analytics Tags 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 f96335cb91d..d42dbfb3ec1 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 @@ -81,7 +81,7 @@ private Future> failedAction(ModuleContex final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags( bidder, outcome, executionTime); - return noAction(moduleContext, analyticsTags); + return noActionResponse(moduleContext, analyticsTags); } private static boolean hasEnrichmentData(TargetingResult targetingResult) { @@ -97,6 +97,27 @@ private static long calcExecutionTime(ModuleContext moduleContext) { } private Future> noAction(ModuleContext moduleContext, Tags analyticsTags) { + final Future targetingCall = moduleContext.getOptableTargetingCall(); + if (targetingCall != null) { + return targetingCall.compose(targetingResult -> { + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(analyticsTags) + .moduleContext(moduleContext) + .build()); + }); + } + + return noActionResponse(moduleContext, analyticsTags); + } + + private Future> noActionResponse(ModuleContext moduleContext, + Tags analyticsTags) { + return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) 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 1f98c18cc0d..8b0e233a003 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 @@ -80,10 +80,7 @@ private Future> enrichedById5SignatureP final String id5Signature = moduleContext.getId5Signature(); - return moduleContext.getOptableTargetingCall() - .compose(targetingResult -> - update(id5SignatureEnrichmentChain(id5Signature), moduleContext)) - .recover(throwable -> success(moduleContext)); + return update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } private PayloadUpdate fullEnrichmentChain(final List targeting, @@ -112,13 +109,8 @@ private Future> update( } private Future> success(ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .moduleContext(moduleContext) - .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) - .build()); + final String id5Signature = moduleContext.getId5Signature(); + return update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } @Override diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index 564d2c4a142..4349ad21240 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -17,11 +17,11 @@ public class Id5Resolver { - public static final String STR_OPTABLE_CO = "optable.co"; - public static final String STR_ID_5_SYNC_COM = "id5-sync.com"; - public static final String STR_OPTABLE = "optable"; - public static final String STR_REF = "ref"; - public static final String STR_SIGNATURE = "signature"; + public static final String OPTABLE_INSERTER = "optable.co"; + public static final String ID5_SOURCE = "id5-sync.com"; + public static final String OPTABLE = "optable"; + public static final String REF = "ref"; + public static final String SIGNATURE = "signature"; private Id5Resolver() { } @@ -37,15 +37,15 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .map(User::getEids) .orElseGet(List::of) .stream() - .filter(it -> STR_OPTABLE_CO.equals(it.getInserter()) && STR_ID_5_SYNC_COM.equals(it.getSource())) + .filter(it -> OPTABLE_INSERTER.equals(it.getInserter()) && ID5_SOURCE.equals(it.getSource())) .map(Eid::getUids) .filter(Objects::nonNull) .flatMap(Collection::stream) .map(Uid::getExt) .filter(Objects::nonNull) - .map(it -> it.get(STR_OPTABLE)) + .map(it -> it.get(OPTABLE)) .filter(Objects::nonNull) - .map(it -> it.get(STR_REF)) + .map(it -> it.get(REF)) .filter(Objects::nonNull) .map(JsonNode::asText) .toList(); @@ -62,9 +62,10 @@ public static String resolveId5Signature(TargetingResult targetingResult) { return refs.stream() .map(references::get) .filter(Objects::nonNull) - .map(it -> it.get(STR_SIGNATURE)) + .map(it -> it.get(SIGNATURE)) .filter(Objects::nonNull) .filter(JsonNode::isValueNode) + .filter(it -> !it.isNull()) .map(JsonNode::asText) .filter(StringUtils::isNotBlank) .findFirst() 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 ad3ba1efefd..0417855a6ff 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 @@ -79,9 +79,7 @@ private static String buildIdsString(List ids, String idPrefixOrder) { final StringBuilder sb = new StringBuilder(); for (Id id : reorderedIds) { sb.append("&id="); - sb.append(URLEncoder.encode( - "%s:%s".formatted(id.getName(), id.getValue()), - StandardCharsets.UTF_8)); + sb.append(URLEncoder.encode(String.format("%s:%s", id.getName(), id.getValue()), StandardCharsets.UTF_8)); } return sb.toString(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 709b18979f5..8a93df9fbdf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java @@ -29,7 +29,7 @@ public class TargetingRequestExecutor { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; private final TimeoutFactory timeoutFactory; - private double logSamplingRate; + private final double logSamplingRate; public TargetingRequestExecutor(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask, 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 4180f8645cc..9ae7f6e4d96 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 @@ -78,6 +78,61 @@ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() { assertThat(result.moduleContext()).isSameAs(moduleContext); } + @Test + public void shouldSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenOptableTargetingProperties(false)); + moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult)); + 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).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + } + + @Test + public void shouldSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndTargetingCallIsPresent() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("otherBidder")); + moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult)); + 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).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + } + @Test public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() { // given 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 5b37eeec80f..19632668f63 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 @@ -59,7 +59,7 @@ public void shouldHaveCode() { } @Test - public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { + public void shouldReturnResultWithUpdateActionAndWithPBSAnalyticsTags() { // given when(invocationContext.moduleContext()).thenReturn( givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); @@ -75,7 +75,7 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { final InvocationResult result = future.result(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); assertThat(result.analyticsTags()) .extracting(Tags::activities) .extracting(List::getFirst) @@ -240,7 +240,7 @@ public void shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargeting } @Test - public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + public void shouldReturnUpdateActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { // given when(invocationContext.moduleContext()).thenReturn( givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); @@ -255,11 +255,11 @@ public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmpty assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); } @Test - public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { + public void shouldReturnResultWithUpdateActionWhenTargetingCallFailsAndNoBids() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( new Audience( @@ -279,27 +279,36 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); } @Test - public void shouldReturnSuccessWhenSkipEnrichmentIsTrue() { + public void shouldReturnUpdateActionWithId5SignatureWhenSkipEnrichmentIsTrue() { // given + final String signature = "id5Signature"; final ModuleContext moduleContext = givenModuleContext(); moduleContext.setShouldSkipEnrichment(true); + moduleContext.setId5Signature(signature); when(invocationContext.moduleContext()).thenReturn(moduleContext); // when final Future> future = target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); } private ObjectNode givenAccountConfig(boolean cacheEnabled) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java index c38a0821d49..41dc2b83ff7 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -1,9 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Uid; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; @@ -16,6 +18,13 @@ public class Id5ResolverTest { + private ObjectMapper mapper; + + @BeforeEach + public void setUp() { + mapper = ObjectMapperProvider.mapper(); + } + @Test public void shouldReturnNullWhenTargetingResultIsNull() { // when @@ -75,8 +84,8 @@ public void shouldReturnNullWhenEidsDoNotMatchOptableAndId5Source() { @Test public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -99,8 +108,8 @@ public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() { @Test public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -108,8 +117,8 @@ public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("otherRef", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("otherRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("signatureValue"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -126,8 +135,8 @@ public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { @Test public void shouldReturnSignatureWhenAllConditionsAreMet() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -135,8 +144,8 @@ public void shouldReturnSignatureWhenAllConditionsAreMet() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() .set("signature", TextNode.valueOf("signatureValue"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -153,12 +162,12 @@ public void shouldReturnSignatureWhenAllConditionsAreMet() { @Test public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { // given - final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); - firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode firstUidExt = mapper.createObjectNode(); + firstUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("firstRef"))); - final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); - secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode secondUidExt = mapper.createObjectNode(); + secondUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("secondRef"))); final Eid firstEid = Eid.builder() @@ -171,10 +180,10 @@ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("firstRef", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("firstRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("firstSignature"))); - refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + refs.set("secondRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("secondSignature"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -191,12 +200,12 @@ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { @Test public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { // given - final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); - firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode firstUidExt = mapper.createObjectNode(); + firstUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("firstRef"))); - final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); - secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode secondUidExt = mapper.createObjectNode(); + secondUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("secondRef"))); final Eid firstEid = Eid.builder() @@ -209,8 +218,8 @@ public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("secondRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("secondSignature"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -227,8 +236,8 @@ public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { @Test public void shouldReturnNullWhenRefEntrySignatureIsBlank() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -236,8 +245,8 @@ public void shouldReturnNullWhenRefEntrySignatureIsBlank() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() .set("signature", TextNode.valueOf(" "))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -254,8 +263,8 @@ public void shouldReturnNullWhenRefEntrySignatureIsBlank() { @Test public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -263,9 +272,9 @@ public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() - .set("signature", ObjectMapperProvider.mapper().createObjectNode())); + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() + .set("signature", mapper.createObjectNode())); final TargetingResult targetingResult = new TargetingResult( List.of(), new Ortb2(new User(List.of(eid), null)), @@ -281,8 +290,8 @@ public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { @Test public void shouldReturnNullWhenRefEntryHasNoSignature() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -290,8 +299,8 @@ public void shouldReturnNullWhenRefEntryHasNoSignature() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() .set("other", TextNode.valueOf("value"))); final TargetingResult targetingResult = new TargetingResult( List.of(), From 43823e94c9fbc483f9a08f9620717cc552694b4e Mon Sep 17 00:00:00 2001 From: softcoder Date: Tue, 28 Jul 2026 20:30:32 +0200 Subject: [PATCH 44/56] optable-targeting: Update cache key --- .../optable/targeting/model/Query.java | 4 +- .../targeting/v1/core/QueryBuilder.java | 26 +++- .../targeting/v1/net/CachedAPIClient.java | 11 +- .../optable/targeting/v1/BaseOptableTest.java | 2 +- .../targeting/v1/core/QueryBuilderTest.java | 134 ++++++++++++++++++ .../targeting/v1/net/CachedAPIClientTest.java | 89 ++++++++++++ 6 files changed, 261 insertions(+), 5 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java index 19b725ab666..9b24ed2ed9d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -11,7 +11,9 @@ public class Query { String attributes; + String hidAttributes; + public String toQueryString() { - return ids + hid + attributes; + return ids + hid + attributes + hidAttributes; } } 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 0417855a6ff..3278fb8de1a 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 @@ -41,7 +41,8 @@ public static Query build(List ids, OptableAttributes optableAttributes, return Query.of( buildIdsString(ids, idPrefixOrder), buildHidString(ids, hidPrefixes), - buildAttributesString(optableAttributes)); + buildAttributesString(optableAttributes), + buildHidAttributesString(optableAttributes)); } private static String buildHidString(List ids, String hidPrefixesString) { @@ -138,4 +139,27 @@ private static String buildAttributesString(OptableAttributes optableAttributes) return sb.toString(); } + + private static String buildHidAttributesString(OptableAttributes optableAttributes) { + final StringBuilder sb = new StringBuilder(); + + Optional.ofNullable(optableAttributes.getApp()) + .ifPresent(app -> { + final String bundle = app.getBundle(); + if (StringUtils.isNotEmpty(bundle)) { + sb.append("&bundle=").append(URLEncoder.encode(bundle, StandardCharsets.UTF_8)); + + final String ver = app.getVer(); + if (StringUtils.isNotEmpty(ver)) { + sb.append("&ver=").append(URLEncoder.encode(ver, StandardCharsets.UTF_8)); + } + } + }); + + Optional.ofNullable(optableAttributes.getId5Signature()) + .ifPresent(id5Signature -> sb.append("&id5_signature=") + .append(URLEncoder.encode(id5Signature, StandardCharsets.UTF_8))); + + return sb.toString(); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index eabccfdbff0..ada03ab1528 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -53,12 +53,19 @@ public Future getTargeting(OptableTargetingProperties propertie } private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { - return "%s:%s:%s:%s".formatted( + return "%s:%s:%s:%s:%s:%s".formatted( tenant, origin, ips.getFirst(), encodeQuery ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) - : query.getIds()); + : query.getIds(), + encodeQuery && query.getHid() != null + ? URLEncoder.encode(query.getHid(), StandardCharsets.UTF_8) + : query.getHid(), + encodeQuery && query.getHidAttributes() != null + ? URLEncoder.encode(query.getHidAttributes(), StandardCharsets.UTF_8) + : query.getHidAttributes() + ); } } 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 2ebf65c7b6e..77ccb5be730 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 @@ -301,7 +301,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, } protected Query givenQuery() { - return Query.of("?que", "r", "y"); + return Query.of("?que", "r", "y", ""); } protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { 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 08f6ce82815..4b329d6c136 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 @@ -336,6 +336,140 @@ public void shouldNotAppendBundleAndVerWhenAppIsNull() { assertThat(query).doesNotContain("&bundle=", "&ver="); } + @Test + public void shouldBuildHidAttributesWithBundleAndVerWhenAppIsPresent() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()) + .isEqualTo("&bundle=com.example.app&ver=1.2.3"); + } + + @Test + public void shouldBuildHidAttributesWithBundleOnlyWhenVerIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEqualTo("&bundle=com.example.app"); + } + + @Test + public void shouldNotBuildHidAttributesWithBundleWhenBundleIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).doesNotContain("&bundle=", "&ver="); + } + + @Test + public void shouldNotBuildHidAttributesWithBundleWhenAppIsNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEmpty(); + } + + @Test + public void shouldBuildHidAttributesWithId5SignatureWhenPresent() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .id5Signature("signature") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEqualTo("&id5_signature=signature"); + } + + @Test + public void shouldNotBuildHidAttributesWithId5SignatureWhenNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).doesNotContain("&id5_signature="); + } + + @Test + public void shouldIncludeHidAttributesInToQueryString() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "1.2.3")) + .id5Signature("signature") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String queryString = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(queryString).endsWith("&bundle=com.example.app&ver=1.2.3&id5_signature=signature"); + } + + @Test + public void shouldAppendBundleAndVerToHidAttributesWithSpecialCharacters() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example app", "1.2 3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()) + .isEqualTo("&bundle=com.example+app&ver=1.2+3"); + } + + @Test + public void shouldAppendId5SignatureToHidAttributesWithSpecialCharacters() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .id5Signature("a b&c=d") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEqualTo("&id5_signature=a+b%26c%3Dd"); + } + @Test public void shouldAppendId5SignatureWhenPresent() { // given diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java index e624fa56a8c..db7542c3b32 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.execution.timeout.Timeout; @@ -13,6 +14,8 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -170,4 +173,90 @@ public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { assertThat(result.getAudience()).isNull(); verify(cache, times(1)).put(any(), eq(targetingResult.result()), anyInt()); } + + @Test + public void shouldIncludeHidInCacheKey() { + // given + final Query query = Query.of("&id=e%3Aemail", "&hid=c:234", "&gdpr=1", ""); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).contains(URLEncoder.encode("&hid=c:234", StandardCharsets.UTF_8)); + } + + @Test + public void shouldIncludeHidAttributesInCacheKey() { + // given + final Query query = Query.of("&id=e%3Aemail", "", "&gdpr=1", "&bundle=com.example.app"); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).contains(URLEncoder.encode("&bundle=com.example.app", StandardCharsets.UTF_8)); + } + + @Test + public void shouldBuildCacheKeyWithAllComponents() { + // given + final Query query = Query.of("&id=e%3Aemail", "&hid=c:234", "&gdpr=1", "&id5_signature=sig"); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties("key", "accountId", "origin", true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).isEqualTo( + "accountId:origin:8.8.8.8:" + + URLEncoder.encode("&id=e%3Aemail", StandardCharsets.UTF_8) + + ":" + + URLEncoder.encode("&hid=c:234", StandardCharsets.UTF_8) + + ":" + + URLEncoder.encode("&id5_signature=sig", StandardCharsets.UTF_8)); + } + + @Test + public void shouldUseNullInCacheKeyWhenHidIsNull() { + // given + final Query query = Query.of("&id=e%3Aemail", null, "&gdpr=1", null); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).endsWith(":null:null"); + } } From e837e3b0d8267ac0fedb5d9c646b552698355bf6 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 29 Jul 2026 12:28:12 +0200 Subject: [PATCH 45/56] optable-targeting: Replace em-dashes in README --- extra/modules/optable-targeting/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 1dae9829156..586627d1288 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -295,20 +295,20 @@ the Targeting API: For app traffic (requests with an `app` object) the module forwards the application's bundle identifier as the `bundle=` query parameter and, when available, its version as `ver=`. Both values are URL-encoded. The `ver` parameter -is only sent when `bundle` is present and non-empty — requests with a version but no bundle will not produce either +is only sent when `bundle` is present and non-empty; requests with a version but no bundle will not produce either parameter. ### ID5 signature The ID5 signature is propagated to the Targeting API in two ways: -1. **Request-side signature** — when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value +1. **Request-side signature**: when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value is sent to the Targeting API as the `id5_signature=` query parameter. -2. **Response-side signature** — when the Targeting API response contains refs with an ID5 signature, the module +2. **Response-side signature**: when the Targeting API response contains refs with an ID5 signature, the module resolves the signature through the matching optable-eid and propagates it back into the request context, where it is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use. -The `id5_signature` field is also part of the Optable input erasure — see above. +The `id5_signature` field is also part of the Optable input erasure, see above. ## Analytics Tags From 05e043568554dc17d8a19c46b05735bd63aa52a7 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 15:30:44 +0200 Subject: [PATCH 46/56] optable-targeting: Fix double rendering of query attributes --- .../targeting/v1/core/QueryBuilder.java | 23 ++++--------------- 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 3278fb8de1a..7220d138c55 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 @@ -120,26 +120,13 @@ private static String buildAttributesString(OptableAttributes optableAttributes) sb.append("&osdk=").append(REQUEST_SOURCE); - Optional.ofNullable(optableAttributes.getApp()) - .ifPresent(app -> { - final String bundle = app.getBundle(); - if (StringUtils.isNotEmpty(bundle)) { - sb.append("&bundle=").append(URLEncoder.encode(bundle, StandardCharsets.UTF_8)); - - final String ver = app.getVer(); - if (StringUtils.isNotEmpty(ver)) { - sb.append("&ver=").append(URLEncoder.encode(ver, StandardCharsets.UTF_8)); - } - } - }); - - Optional.ofNullable(optableAttributes.getId5Signature()) - .ifPresent(id5Signature -> sb.append("&id5_signature=") - .append(URLEncoder.encode(id5Signature, StandardCharsets.UTF_8))); - return sb.toString(); } - + + /** + * Kept apart from buildAttributesString because these are the only attributes that + * discriminate one user from another, so they have to take part in the cache key. + */ private static String buildHidAttributesString(OptableAttributes optableAttributes) { final StringBuilder sb = new StringBuilder(); From 7806a543204fa7f59b2261f8c310676630e2c2cc Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 15:46:08 +0200 Subject: [PATCH 47/56] optable-targeting: add Targeting API failure branch to bidder request hook --- .../optable/targeting/v1/OptableBidderRequestHook.java | 10 ++-------- .../optable/targeting/v1/core/QueryBuilder.java | 2 +- 2 files changed, 3 insertions(+), 9 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 d42dbfb3ec1..430d80247b5 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 @@ -102,14 +102,8 @@ private Future> noAction(ModuleContext mo return targetingCall.compose(targetingResult -> { moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .analyticsTags(analyticsTags) - .moduleContext(moduleContext) - .build()); - }); + return noActionResponse(moduleContext, analyticsTags); + }).recover(ignored -> noActionResponse(moduleContext, analyticsTags)); } return noActionResponse(moduleContext, analyticsTags); 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 7220d138c55..78d362d9fec 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 @@ -122,7 +122,7 @@ private static String buildAttributesString(OptableAttributes optableAttributes) return sb.toString(); } - + /** * Kept apart from buildAttributesString because these are the only attributes that * discriminate one user from another, so they have to take part in the cache key. From 86d8d5659bb3dd4784e69c3ea9e09ce78a1c0273 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 16:04:22 +0200 Subject: [PATCH 48/56] optable-targeting: Remove redundant dependency --- .../optable/targeting/config/OptableTargetingConfig.java | 2 +- .../hooks/modules/optable/targeting/v1/core/IdsMapper.java | 5 +---- .../modules/optable/targeting/v1/core/IdsMapperTest.java | 2 +- 3 files changed, 3 insertions(+), 6 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 91ae6691bbf..ef36467d2b5 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 @@ -48,7 +48,7 @@ OptableTargetingProperties optableTargetingProperties() { @Bean IdsMapper queryParametersExtractor(@Value("${logging.sampling-rate:0.01}") double logSamplingRate) { - return new IdsMapper(ObjectMapperProvider.mapper(), logSamplingRate); + return new IdsMapper(logSamplingRate); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index ddb47141248..2ea248334b3 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -1,6 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.fasterxml.jackson.databind.ObjectMapper; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Eid; @@ -27,11 +26,9 @@ public class IdsMapper { "utiq.com", Id.UTIQ, "netid.de", Id.NET_ID); - private final ObjectMapper objectMapper; private final double logSamplingRate; - public IdsMapper(ObjectMapper objectMapper, double logSamplingRate) { - this.objectMapper = Objects.requireNonNull(objectMapper); + public IdsMapper(double logSamplingRate) { this.logSamplingRate = logSamplingRate; } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java index 714fc538a34..ee273761f04 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java @@ -32,7 +32,7 @@ public class IdsMapperTest { @BeforeEach public void setUp() { - target = new IdsMapper(objectMapper, 0.01); + target = new IdsMapper(0.01); } @Test From 91d62233e991aa163b419acd15cd1d6f9063607e Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 17:49:29 +0200 Subject: [PATCH 49/56] optable-targeting: improve tests coverage --- .../v1/OptableBidderRequestHookTest.java | 19 +++++++++++++++ .../targeting/v1/core/Id5ResolverTest.java | 23 +++++++++++++++++++ .../targeting/v1/core/QueryBuilderTest.java | 16 +++++++++++++ 3 files changed, 58 insertions(+) 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 9ae7f6e4d96..3c2d1c660ab 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 @@ -78,6 +78,25 @@ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() { assertThat(result.moduleContext()).isSameAs(moduleContext); } + @Test + public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabledAndTargetingCallFailed() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenOptableTargetingProperties(false)); + moduleContext.setOptableTargetingCall(Future.failedFuture(new RuntimeException("timeout"))); + 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).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(moduleContext.getId5Signature()).isNull(); + } + @Test public void shouldSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() { // given diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java index 41dc2b83ff7..da0e5bb4161 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -25,6 +25,29 @@ public void setUp() { mapper = ObjectMapperProvider.mapper(); } + @Test + public void shouldReturnNullWhenSignatureIsJsonNull() { + // given + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode().putNull("signature")); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + // then + assertThat(result).isNull(); + } + @Test public void shouldReturnNullWhenTargetingResultIsNull() { // when 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 4b329d6c136..f8eb3a99c36 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 @@ -437,6 +437,22 @@ public void shouldIncludeHidAttributesInToQueryString() { // then assertThat(queryString).endsWith("&bundle=com.example.app&ver=1.2.3&id5_signature=signature"); + // they live in hidAttributes so that they take part in the cache key, and must not + // also be emitted from buildAttributesString + assertThat(queryString.split("&bundle=", -1)).hasSize(2); + assertThat(queryString.split("&ver=", -1)).hasSize(2); + assertThat(queryString.split("&id5_signature=", -1)).hasSize(2); + } + + @Test + public void shouldTrimWhitespaceAroundHidPrefixes() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "phone")); + // when + final Query query = QueryBuilder.build( + ids, OptableAttributes.builder().build(), givenProperties(idPrefixOrder, " e , p ")); + // then + assertThat(query.getHid()).isEqualTo("&hid=e:email&hid=p:phone"); } @Test From 57c84e7db32fd97c7cd3e5b03b85fcf345752423 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 17:59:45 +0200 Subject: [PATCH 50/56] optable-targeting: don't add blank id5_signature into a query --- .../optable/targeting/v1/core/OptableAttributesResolver.java | 1 + 1 file changed, 1 insertion(+) 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 39783e9bf34..0828adea34d 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 @@ -68,6 +68,7 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .map(ext -> ext.getProperty("optable")) .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate)) .map(ExtUserOptable::getId5Signature) + .filter(StringUtils::isNotBlank) .ifPresent(builder::id5Signature); return builder.build(); From 16db8f7155a2daa5bae3df1122ab804fe2367a75 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 19:02:20 +0200 Subject: [PATCH 51/56] optable-targeting: remove id5_signature resolving for bidders which are not participate in targeting enrichment process. --- .../targeting/v1/OptableBidderRequestHook.java | 17 ++--------------- .../v1/OptableBidderRequestHookTest.java | 8 ++++---- 2 files changed, 6 insertions(+), 19 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 430d80247b5..317cb621fe4 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 @@ -81,7 +81,7 @@ private Future> failedAction(ModuleContex final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags( bidder, outcome, executionTime); - return noActionResponse(moduleContext, analyticsTags); + return noAction(moduleContext, analyticsTags); } private static boolean hasEnrichmentData(TargetingResult targetingResult) { @@ -96,20 +96,7 @@ private static long calcExecutionTime(ModuleContext moduleContext) { return startTime > 0 ? System.currentTimeMillis() - startTime : 0; } - private Future> noAction(ModuleContext moduleContext, Tags analyticsTags) { - final Future targetingCall = moduleContext.getOptableTargetingCall(); - if (targetingCall != null) { - return targetingCall.compose(targetingResult -> { - moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); - - return noActionResponse(moduleContext, analyticsTags); - }).recover(ignored -> noActionResponse(moduleContext, analyticsTags)); - } - - return noActionResponse(moduleContext, analyticsTags); - } - - private Future> noActionResponse(ModuleContext moduleContext, + private Future> noAction(ModuleContext moduleContext, Tags analyticsTags) { return Future.succeededFuture( 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 3c2d1c660ab..5778ac41289 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 @@ -98,7 +98,7 @@ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabledAndTargetingCal } @Test - public void shouldSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() { + public void shouldNotSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() { // given final String refValue = "refValue"; final String signature = "id5Signature"; @@ -121,11 +121,11 @@ public void shouldSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisable assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.no_action, InvocationResult::action); - assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + assertThat(moduleContext.getId5Signature()).isNull(); } @Test - public void shouldSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndTargetingCallIsPresent() { + public void shouldNotSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndTargetingCallIsPresent() { // given final String refValue = "refValue"; final String signature = "id5Signature"; @@ -149,7 +149,7 @@ public void shouldSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndT assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.no_action, InvocationResult::action); - assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + assertThat(moduleContext.getId5Signature()).isNull(); } @Test From 571c7e536a053b4ce81328abb18c2ac94c9a991f Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 30 Jul 2026 19:55:47 +0200 Subject: [PATCH 52/56] optable-targeting: render id5_signature only if targeting enabled --- .../OptableTargetingAuctionResponseHook.java | 32 +++++-------- ...tableTargetingAuctionResponseHookTest.java | 45 ++++++++++++++----- 2 files changed, 45 insertions(+), 32 deletions(-) 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 8b0e233a003..0b94e1bf6be 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 @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; 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; @@ -57,32 +56,18 @@ public Future> call(AuctionResponsePayl return success(moduleContext); } + final List targeting = moduleContext.getTargeting(); + final String id5Signature = moduleContext.getId5Signature(); + final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( - auctionResponsePayload.bidResponse(), moduleContext.getTargeting()); + auctionResponsePayload.bidResponse(), targeting); moduleContext.setEnrichResponseStatus(validationStatus); return validationStatus.getStatus() == Status.SUCCESS - ? enrichedPayload(moduleContext) - : enrichedById5SignaturePayload(moduleContext); - } - - private Future> enrichedPayload(ModuleContext moduleContext) { - final List targeting = moduleContext.getTargeting(); - final String id5Signature = moduleContext.getId5Signature(); - - return CollectionUtils.isNotEmpty(targeting) ? update(fullEnrichmentChain(targeting, id5Signature), moduleContext) : update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } - private Future> enrichedById5SignaturePayload( - ModuleContext moduleContext) { - - final String id5Signature = moduleContext.getId5Signature(); - - return update(id5SignatureEnrichmentChain(id5Signature), moduleContext); - } - private PayloadUpdate fullEnrichmentChain(final List targeting, final String id5Signature) { @@ -109,8 +94,13 @@ private Future> update( } private Future> success(ModuleContext moduleContext) { - final String id5Signature = moduleContext.getId5Signature(); - return update(id5SignatureEnrichmentChain(id5Signature), moduleContext); + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .moduleContext(moduleContext) + .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) + .build()); } @Override 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 19632668f63..9bb0d1a7a10 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 @@ -11,6 +11,7 @@ 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.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; @@ -283,32 +284,54 @@ public void shouldReturnResultWithUpdateActionWhenTargetingCallFailsAndNoBids() } @Test - public void shouldReturnUpdateActionWithId5SignatureWhenSkipEnrichmentIsTrue() { + public void shouldReturnNoActionWhenAdserverTargetingIsDisabled() { + // given + final OptableTargetingProperties properties = givenOptableTargetingProperties(false); + properties.setAdserverTargeting(false); + configResolver = new ConfigResolver(mapper, jsonMerger, properties); + target = new OptableTargetingAuctionResponseHook(configResolver, mapper, jsonMerger); + when(invocationContext.accountConfig()).thenReturn(mapper.valueToTree(properties)); + when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.failedFuture(new RuntimeException("error")))); + + // 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); + assertThat(result.payloadUpdate()).isNull(); + } + + @Test + public void shouldReturnNoActionWhenSkipEnrichmentIsTrue() { // given - final String signature = "id5Signature"; final ModuleContext moduleContext = givenModuleContext(); moduleContext.setShouldSkipEnrichment(true); - moduleContext.setId5Signature(signature); when(invocationContext.moduleContext()).thenReturn(moduleContext); // when final Future> future = target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); - final BidResponse bidResponse = result - .payloadUpdate() - .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) - .bidResponse(); - final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.update, InvocationResult::action); - assertThat(passthrough).isNotNull(); - assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(result.payloadUpdate()).isNull(); } private ObjectNode givenAccountConfig(boolean cacheEnabled) { From bb3deb32ca7bcb89a72f8565ae59d7390c7fe201 Mon Sep 17 00:00:00 2001 From: softcoder Date: Fri, 31 Jul 2026 12:10:56 +0200 Subject: [PATCH 53/56] optable-targeting: split targeting and id5 features rendering --- .../OptableTargetingAuctionResponseHook.java | 14 +- ...tableTargetingAuctionResponseHookTest.java | 178 +++++++++++++++++- 2 files changed, 182 insertions(+), 10 deletions(-) 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 0b94e1bf6be..5b915618b0c 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 @@ -53,19 +53,18 @@ public Future> call(AuctionResponsePayl moduleContext.setAdserverTargetingEnabled(adserverTargeting); if (moduleContext.isShouldSkipEnrichment() || !adserverTargeting) { - return success(moduleContext); + return id5SignatureOnlyPayload(moduleContext); } final List targeting = moduleContext.getTargeting(); - final String id5Signature = moduleContext.getId5Signature(); final EnrichmentStatus validationStatus = AuctionResponseValidator.checkEnrichmentPossibility( auctionResponsePayload.bidResponse(), targeting); moduleContext.setEnrichResponseStatus(validationStatus); return validationStatus.getStatus() == Status.SUCCESS - ? update(fullEnrichmentChain(targeting, id5Signature), moduleContext) - : update(id5SignatureEnrichmentChain(id5Signature), moduleContext); + ? update(fullEnrichmentChain(targeting, moduleContext.getId5Signature()), moduleContext) + : id5SignatureOnlyPayload(moduleContext); } private PayloadUpdate fullEnrichmentChain(final List targeting, @@ -75,6 +74,13 @@ private PayloadUpdate fullEnrichmentChain(final List> id5SignatureOnlyPayload(ModuleContext moduleContext) { + final String id5Signature = moduleContext.getId5Signature(); + return id5Signature != null + ? update(id5SignatureEnrichmentChain(id5Signature), moduleContext) + : success(moduleContext); + } + private PayloadUpdate id5SignatureEnrichmentChain(String id5Signature) { return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger); } 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 9bb0d1a7a10..948b1b50675 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -60,7 +60,7 @@ public void shouldHaveCode() { } @Test - public void shouldReturnResultWithUpdateActionAndWithPBSAnalyticsTags() { + public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTagsWhenTargetingIsEmptyAndNoId5Signature() { // given when(invocationContext.moduleContext()).thenReturn( givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); @@ -76,7 +76,8 @@ public void shouldReturnResultWithUpdateActionAndWithPBSAnalyticsTags() { final InvocationResult result = future.result(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.update, InvocationResult::action); + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(result.payloadUpdate()).isNull(); assertThat(result.analyticsTags()) .extracting(Tags::activities) .extracting(List::getFirst) @@ -88,6 +89,36 @@ public void shouldReturnResultWithUpdateActionAndWithPBSAnalyticsTags() { assertThat(result.errors()).isNull(); } + @Test + public void shouldReturnResultWithUpdateActionAndId5SignatureWhenTargetingIsEmptyButId5SignatureIsPresent() { + // given + final String signature = "id5Signature"; + final ModuleContext moduleContext = + givenModuleContext(null, Future.failedFuture(new RuntimeException("error"))); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + @Test public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn() { // given @@ -241,7 +272,7 @@ public void shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargeting } @Test - public void shouldReturnUpdateActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmptyAndNoId5Signature() { // given when(invocationContext.moduleContext()).thenReturn( givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); @@ -251,16 +282,47 @@ public void shouldReturnUpdateActionWhenOptableTargetingCallFailsAndTargetingIsE 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); + assertThat(result.payloadUpdate()).isNull(); + } + + @Test + public void shouldReturnUpdateActionWithId5SignatureWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + // given + final String signature = "id5Signature"; + final ModuleContext moduleContext = + givenModuleContext(null, Future.failedFuture(new RuntimeException("error"))); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); } @Test - public void shouldReturnResultWithUpdateActionWhenTargetingCallFailsAndNoBids() { + public void shouldReturnNoActionWhenTargetingCallFailsAndNoBidsAndNoId5Signature() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( new Audience( @@ -275,16 +337,52 @@ public void shouldReturnResultWithUpdateActionWhenTargetingCallFailsAndNoBids() 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); + assertThat(result.payloadUpdate()).isNull(); + } + + @Test + public void shouldReturnUpdateActionWithId5SignatureWhenTargetingCallFailsAndNoBids() { + // given + final String signature = "id5Signature"; + final ModuleContext moduleContext = givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.failedFuture(new RuntimeException("error"))); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); } @Test - public void shouldReturnNoActionWhenAdserverTargetingIsDisabled() { + public void shouldReturnNoActionWhenAdserverTargetingIsDisabledAndNoId5Signature() { // given final OptableTargetingProperties properties = givenOptableTargetingProperties(false); properties.setAdserverTargeting(false); @@ -314,7 +412,46 @@ public void shouldReturnNoActionWhenAdserverTargetingIsDisabled() { } @Test - public void shouldReturnNoActionWhenSkipEnrichmentIsTrue() { + public void shouldReturnUpdateActionWithId5SignatureWhenAdserverTargetingIsDisabled() { + // given + final String signature = "id5Signature"; + final OptableTargetingProperties properties = givenOptableTargetingProperties(false); + properties.setAdserverTargeting(false); + configResolver = new ConfigResolver(mapper, jsonMerger, properties); + target = new OptableTargetingAuctionResponseHook(configResolver, mapper, jsonMerger); + when(invocationContext.accountConfig()).thenReturn(mapper.valueToTree(properties)); + final ModuleContext moduleContext = givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.failedFuture(new RuntimeException("error"))); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + + @Test + public void shouldReturnNoActionWhenSkipEnrichmentIsTrueAndNoId5Signature() { // given final ModuleContext moduleContext = givenModuleContext(); moduleContext.setShouldSkipEnrichment(true); @@ -334,6 +471,35 @@ public void shouldReturnNoActionWhenSkipEnrichmentIsTrue() { assertThat(result.payloadUpdate()).isNull(); } + @Test + public void shouldReturnUpdateActionWithId5SignatureWhenSkipEnrichmentIsTrue() { + // given + final String signature = "id5Signature"; + final ModuleContext moduleContext = givenModuleContext(); + moduleContext.setShouldSkipEnrichment(true); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + private ObjectNode givenAccountConfig(boolean cacheEnabled) { return mapper.valueToTree(givenOptableTargetingProperties(cacheEnabled)); } From 0fa2141853f6a023c5fb8578b516377b1f09d2a5 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 31 Jul 2026 12:37:56 +0200 Subject: [PATCH 54/56] optable-targeting: fix NoBids test to use a bid-less response The test named for the NOBID branch stubbed a bid response that has a bid, so with non-empty targeting the validator returned SUCCESS and the test exercised the full enrichment chain instead of the signature-only one. --- .../v1/OptableTargetingAuctionResponseHookTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 948b1b50675..08c1d4d3997 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 @@ -359,7 +359,8 @@ public void shouldReturnUpdateActionWithId5SignatureWhenTargetingCallFailsAndNoB Future.failedFuture(new RuntimeException("error"))); moduleContext.setId5Signature(signature); when(invocationContext.moduleContext()).thenReturn(moduleContext); - when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + final BidResponse bidlessResponse = BidResponse.builder().build(); + when(auctionResponsePayload.bidResponse()).thenReturn(bidlessResponse); // when final Future> future = @@ -367,7 +368,7 @@ public void shouldReturnUpdateActionWithId5SignatureWhenTargetingCallFailsAndNoB final InvocationResult result = future.result(); final BidResponse bidResponse = result .payloadUpdate() - .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .apply(AuctionResponsePayloadImpl.of(bidlessResponse)) .bidResponse(); final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); From d1b2df5597e9a8311f71e6760808320be6570a33 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 14 Aug 2026 12:27:21 +0200 Subject: [PATCH 55/56] enable more bidders for smoke tests --- sample/configs/prebid-config-with-optable.yaml | 14 +++++++++++++- sample/configs/sample-app-settings-optable.yaml | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index ae85e0ad69f..93ebd387348 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -13,9 +13,21 @@ adapters: improvedigital: enabled: true colossus: - enabled: true + enabled: true triplelift: enabled: true + adf: + enabled: true + yieldmo: + enabled: true + grid: + enabled: true + unruly: + enabled: true + epsilon: + enabled: true + thirtythreeacross: + enabled: true metrics: prefix: prebid cache: diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index b05448d65ca..58238287f58 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -30,7 +30,7 @@ accounts: enrich-web: true enrich-app: true id-prefix-order: "e,v,c" - hid-prefixes: "c,i6" + hid-prefixes: "a,c,i6" ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: From 5b54b45bccbe2ad147b3b010b0a88cb625655094 Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Fri, 14 Aug 2026 14:10:58 +0200 Subject: [PATCH 56/56] remove and .gitignore AI memory and vscode files --- .cursorrules | 262 -------------------------------- .gemini/settings.json | 15 -- .gitignore | 10 ++ GEMINI.md | 9 -- extra/.vscode_old/launch.json | 36 ----- extra/.vscode_old/settings.json | 18 --- extra/.vscode_old/tasks.json | 33 ---- 7 files changed, 10 insertions(+), 373 deletions(-) delete mode 100644 .cursorrules delete mode 100644 .gemini/settings.json delete mode 100644 GEMINI.md delete mode 100644 extra/.vscode_old/launch.json delete mode 100644 extra/.vscode_old/settings.json delete mode 100644 extra/.vscode_old/tasks.json diff --git a/.cursorrules b/.cursorrules deleted file mode 100644 index 9d6f12e9da7..00000000000 --- a/.cursorrules +++ /dev/null @@ -1,262 +0,0 @@ -# Prebid Server Java Coding Rules - -You are an expert Java developer and Professor of Software Engineering, specializing in the Prebid Server Java codebase. You adhere to strict high-quality standards, favoring Modern Java (17+), Vert.x patterns, JUnit 5, Mockito (BDD style), and AssertJ. - -## 1. General Coding Philosophy -- **Non-Blocking I/O**: This is a Vert.x application. NEVER block the Event Loop. Use asynchronous patterns (`Future`, `Promise`) for I/O operations. -- **Immutability**: Prefer immutable data structures. - - Use `final` for all local variables, fields, and parameters. - - **Modern Java:** Use Java 17+ features like `record` and `sealed interface` where appropriate for data carriers and hierarchy restrictions. - - **Var Usage:** `var` is **STRICTLY PROHIBITED** in production code. Use explicit types. `var` is ALLOWED in tests. - - **Final Keyword:** Use `final` for all variables and parameters where possible to ensure immutability. - - **Switch Expressions:** Use modern `switch` expressions with arrow syntax `->`. - - **DTOs / POJOs**: **STRICTLY PROHIBITED** to write manual Getters, Setters, `toString`, `equals`, `hashCode`, or Constructors if Lombok can handle it. - - Use `@Value` + `staticConstructor="of"` for immutable value objects (default). - - Use `@Builder(toBuilder = true)` for complex objects (>4 fields) or when mutation-like copies are needed. - - Use `@AllArgsConstructor` / `@RequiredArgsConstructor` for simple dependency injection or wrappers. -- **Variable Types**: **DO NOT USE `var`**. Always write full variable types (Project Convention). -- **Null Safety**: - - Avoid returning `null`. Use `Optional` for return types. - - Avoid long chains of null checks; use `Optional` or `org.prebid.server.util.ObjectUtil`. -- **Privacy**: **STRICTLY PROHIBITED** to log private data (publishers, exchanges, usage analytics). -- **JSON Handling**: - - **FORBIDDEN**: `io.vertx.core.json.Json` (static Vert.x mapper). - - **ALLOWED**: Inject and use `JacksonMapper` or the project's configured `ObjectMapper`. - -## 3. Libraries & Utilities -- **Apache Commons**: Prefer using existing helpers from `commons-lang3` and `commons-collections4` over writing custom utility methods. - - Used: `StringUtils`, `ObjectUtils`, `CollectionUtils`, `MapUtils`. - - Avoid creating new `*Util` classes if an Apache Commons equivalent exists. - - Example: Use `StringUtils.isBlank(str)` instead of `str == null || str.trim().isEmpty()`. -- **Project Utilities**: - - **Project Utilities:** Use `org.prebid.server.util.ObjectUtil.getIfNotNull()` for concise, null-safe access chains (alternative to `Optional` in hot paths). - - **Apache Commons:** Heavily prefer `StringUtils`, `ObjectUtils`, `CollectionUtils`, `ListUtils`, `MapUtils` for null-safe operations. - - **Validation:** Use `java.util.Objects.requireNonNull(arg)` in constructors to enforce non-null dependencies. - - **Collection Utilities:** Use `java.util.Collections.emptyList()`, `Map.of()`, `Set.of()` etc. for better readability and immutability. - - -## 2. Code Style & Standards (Oracle & Checkstyle) -- **Base Standard**: Adhere to [Oracle Java Code Conventions](https://www.oracle.com/docs/tech/java/codeconventions.pdf) unless overridden by project-specific Checkstyle rules. -- **Indentation**: - - **4 spaces** for class members, methods, and blocks. - - **8 spaces** for line continuations (wrapping). -- **Line Length**: Max 120 characters (overrides Oracle's 80). -- **Class Structure** (Oracle Convention): - 1. Class/Instance Variables (Standard Order: public, protected, package, private). - 2. Constructors. - 3. Methods (Grouped by functionality, logic flow, or readability). -- **Declarations**: - - One variable declaration per line. - - Initialize variables at declaration where possible. - - Place declarations at the beginning of blocks (legacy Oracle) OR near first usage (Modern/Clean Code) -> **Prefer near first usage**. -- **Statements**: - - Always use braces `{}` for `if`, `else`, `for`, `do`, `while` (K&R style). - - No parentheses in `return` statements. - -- **Imports**: - - **FORBIDDEN**: Wildcard imports (`import java.util.*;`). - - Order: Third-party libraries first, then `java.*`/`jakarta.*` at the bottom (separated by blank line). - - No `static` imports in production code (except standard utilities if really needed), but allowed in Tests. - - **Illegal Imports**: `org.junit.Test` (JUnit 4), `org.apache.commons.lang` (use `lang3`), `io.vertx.core.json.Json`. -- **Naming**: - - Constants: `UPPER_CASE_WITH_UNDERSCORES`. - - Methods/Fields: `camelCase`. - - **Maps**: Use `keyToValue` convention (e.g., `impToExt`, `accountIdToBidder`). - - **Self-Explanatory**: Avoid single-letter variables. `resolvedParam` > `s`. -- **Collections**: - - Use literals/factories: `List.of()`, `Collections.emptyList()`, `Collections.singletonList()`. -- **Formatting**: - - Parenthesis on expression end. - - Ternary operators: Long ones on separate lines, short ones on one line. - - Boolean logic: Explicit parenthesis for precedence `(a && b) || c`. - - **Method Ordering**: Call order (Interface method -> private methods it calls -> next Interface method). -- **Dependencies**: Do not call methods from transitive dependencies; declare them explicitly in `pom.xml`. -- **Lombok**: - - Use `@Value` + `staticConstructor="of"`. - - Use `@Builder` for constructors with > 4 arguments. - - `toBuilder = true` for updates. - -## 4. Architecture, SOLID & Design Patterns - -### 4.1 SOLID Principles -- **SRP (Single Responsibility)**: - - Classes must have one clearly defined purpose. - - **Refactor**: Split large "God Classes" into smaller delegates or services. -- **OCP (Open/Closed)**: - - Design for extension. Use Interfaces and Strategy patterns so new behavior can be added without changing existing code. -- **LSP (Liskov Substitution)**: - - Subclasses/Implementations must behave consistently. Do not throw `UnsupportedOperationException` unexpectedly. -- **ISP (Interface Segregation)**: - - Prefer focused interfaces (e.g., `Reader`, `Writer`) over large broad ones. -- **DIP (Dependency Inversion)**: - - Depend on abstractions (Interfaces). - - **Framework**: Use **Spring Framework** for Dependency Injection. - - **Injection Style**: **Constructor Injection** is REQUIRED. Field injection (`@Autowired` on fields) is **STRICTLY PROHIBITED**. - -### 4.2 Clean Code & Best Practices -- **Readability**: Code is read more often than written. Optimize for reading. -- **Naming**: - - **Intent-Revealing**: `daysSinceCreation` > `d`. - - **No Encodings**: No Hungarian notation or type prefixes. -- **Functions**: - - **Small**: Methods should be small and do one thing. - - **Guard Clauses**: Use strict guard clauses/early returns to flatten nesting. - - **Pure Functions**: Prefer `private static` methods for stateless logic. -- **Comments**: - - **Why, not What**: Comments should explain the business decision, not the syntax. - - **No Code Comments**: Do not comment out code; delete it. Git history remembers. - -### 4.3 Architecture & Reactive Vert.x Patterns -- **Reactive Philosophy**: - - **Everything is a Stream/Future**: Treat business logic as a pipeline of transformation steps. - - **Chaining**: Build flows by chaining operator methods (`map`, `compose`, `onContentType`, `recover`). - - **No Callbacks**: "Callback Hell" is strictly prohibited. Use functional composition. -- **Future Composition (The Glue)**: - - **Strict Transformation Pipeline:** Treat business logic as a stream of data transformations. - - **Chaining:** Use `.map()` for synchronous transformations and `.compose()` for asynchronous operations. - - **Parallelism:** Use `Future.all()`, `CompositeFuture.join()`, or `CompositeFuture.all()` for parallel execution. - - **Side Effects:** Use `.onSuccess()`, `.onFailure()`, and `.onComplete()` **ONLY** for side effects (logging, metrics). **NEVER** use them for control flow or chaining logic. - - **Error Handling:** Propagate errors down the chain. Use `.recover()` for falling back or transforming errors. - - **Avoid Callbacks:** **STRICTLY PROHIBIT** nested callbacks or "Callback Hell". logic must be flat. - - **Thread Safety:** Always assume the code runs on the Event Loop. **NEVER** block the thread. - - **Context Awareness:** Pass `RoutingContext` or `AuctionContext` as a carrier of state through the chain. - - ### 4.4 Object-Oriented Patterns - - **Value Objects:** Use Lombok `@Value(staticConstructor = "of")` for immutable data carriers. - - **Factory Methods:** Prefer static factory methods named `of(...)` or `create(...)` over public constructors for complex object creation. - - **NoOp Pattern:** For interfaces that may have empty implementations (e.g., hooks, empty services), create a `NoOpExtension` or `static NoOp` implementation within the interface or as a separate class. - - **Sealed Hierarchies:** Use `sealed interface` and `record` (Java 17+) for restricted class hierarchies (e.g., specialized result types). - - ### 4.5 JSON & Serialization - - **JacksonMapper:** Always use the project's `JacksonMapper` wrapper instead of raw `ObjectMapper` where possible. - - **Dynamic JSON:** Use `ObjectNode` and `ArrayNode` for handling dynamic or unstructured data (especially `ext` fields) rather than untyped `Map`. - - **JsonPointer:** Use `JsonPointer` for safe and readable deep traversal of JSON trees (`node.at("/path/to/field")`). -- **Concurrency (Vert.x Core Rule)**: - - **Single Threaded Event Loop**: The application runs on the Event Loop. - - **PROHIBITED**: `synchronized`, `wait()`, `notify()`, `Thread.sleep()`, `BlockingQueue`, or any blocking Java concurrency primitive. - - **Blocking Code**: If you MUST run blocking code (e.g. legacy JDBC), use `vertx.executeBlocking(...)` but prefer non-blocking clients. - - **ALLOWED**: `Future`, `Promise`, `vertx.setTimer()`. -- **Error Handling**: - - **Async**: In async chains, errors propagate automatically. Do not break the chain. - - **Return Failed Future**: In async methods, return `Future.failedFuture(e)` immediately rather than throwing runtime exceptions. - - **Granularity**: Catch specific exceptions in `.recover()`. Avoid broad catches unless it's a top-level handler. -- **Context**: Be aware of the Vert.x `Context`. Ensure context is preserved when switching threads. -- **Logging**: - - Use standard SLF4J usages: `private static final Logger logger = LoggerFactory.getLogger(MyClass.class);` - - Do not use `System.out.println` or `e.printStackTrace()`. - - Log at `DEBUG` for high-volume messages, `INFO` for lifecycle events, `WARN/ERROR` for unexpected conditions. - -## 5. Testing Rules -- **Frameworks**: - - **JUnit 5**: `org.junit.jupiter.api.*`. - - **Behavior Driven Development (BDD)**: Use `given(mock.method()).willReturn(...)` instead of `when(...)`. - - **Strictness**: Use `@Mock(strictness = Mock.Strictness.LENIENT)` for infrastructure mocks (Metrics, Services) defined in `setUp` but not used in every test to avoid `UnnecessaryStubbingException`. - - **Async Testing**: For `Future`-based tests, use `VertxTestContext`: - ```java - @Test - void shouldSucceed(VertxTestContext context) { - future.onComplete(context.succeeding(result -> { - assertThat(result).isNotNull(); - context.completeNow(); - })); - } - ``` - - **Time Testing**: Mock `java.time.Clock` ensures deterministic time tests. - - **Static Imports**: ALWAYS statically import: - - `org.assertj.core.api.Assertions.*` (assertThat, tuple, entry) - - `org.mockito.BDDMockito.*` (given) - - `org.mockito.Mockito.*` (verify, never, etc.) - - `org.mockito.ArgumentMatchers.*` (any, eq, etc.) - - **Assertions**: - - Use `extracting()` for nested properties. - - Use `containsExactlyInAnyOrder` for lists. - - Use `containsOnly` for verifying single-element logical containment. - - **Structure**: - - Use `target` as the name for the class under test instance. - - Use `setUp()` method annotated with `@BeforeEach`. - - **Vert.x Data**: Use `MultiMap.caseInsensitiveMultiMap()` for testing headers/params. - - Fields: SUT (System Under Test) named `target`. -- **Granularity**: - - 1 Test = 1 Logic Path. Avoid `testFooAndBar`. Split into `testFoo` and `testBar`. - - No `ParameterizedTest` preference; explicitly write separate tests for meaningful scenarios (per docs). -- **Data Placement**: - - **Inline Data**: Place test data INSIDE the test method (local variables). - - **No Constants**: Avoid class-level constants for test data. - - **Fake Data**: Do not use real URLs/IDs (use `test.com`, `id`). -- **Mocking**: - - Annotate test class with `@ExtendWith(MockitoExtension.class)`. - - Use `@Mock` for dependencies. - - Use strict mocking (default). Only use `lenientness` if absolutely necessary for shared setup. -- **Naming**: `methodNameShouldReturnExpectedBehaviorWhenCondition`. - - Example: `processDataShouldReturnResultWhenInputIsData`. -- **BDD Style**: - ```java - // given - given(dependency.call()).willReturn(futureResult); - final var input = "test-input"; // 'var' is acceptable in tests only if brevity aids readability, but prefer explicit types per project rule. - - // when - Future result = target.execute(input); - - // then - assertThat(result.succeeded()).isTrue(); - ``` - -## 6. Implementation Workflow (Agent Instructions) -1. **Analyze**: Read existing code and `pom.xml` to understand dependencies. -2. **Plan**: Propose changes before writing. -3. **Implement**: - - Write logic using functional style (`Stream` API). - - Ensure extensive logging for debuggability (at `debug` or `trace` level for high-volume paths). -4. **Test**: - - ALWAYS write or update unit tests for changed logic. - - Validate logic with `VertxTest` if async/JSON is involved. - -## 7. Example Test Template - -```java -package org.prebid.server.component; - -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.VertxTest; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.verify; - -@ExtendWith(MockitoExtension.class) -class ExampleTest { - - @Mock - private Dependency dependency; - @Mock(strictness = Mock.Strictness.LENIENT) // Common infra mock - private Metrics metrics; - - private Example target; - - @BeforeEach - void setUp() { - target = new Example(dependency, metrics); - } - - @Test - void shouldReturnExpectedValueWhenConditionMet() { - // given - given(dependency.getData()).willReturn("data"); - - // when - final var result = target.process(); - - // then - assertThat(result) - .extracting(Result::getValue) - .isEqualTo("data"); - verify(metrics).updateMetric(any()); - } -} -``` diff --git a/.gemini/settings.json b/.gemini/settings.json deleted file mode 100644 index 44d4be1fc97..00000000000 --- a/.gemini/settings.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "hooks": { - "BeforeTool": [ - { - "matcher": "read_file|list_directory", - "hooks": [ - { - "type": "command", - "command": "python -c \"import sys,pathlib,json;e=pathlib.Path('graphify-out/graph.json').exists();d={'decision':'allow'};e and d.update({'additionalContext':'graphify: Knowledge graph exists. Read graphify-out/GRAPH_REPORT.md for god nodes and community structure before searching raw files.'});sys.stdout.write(json.dumps(d))\"" - } - ] - } - ] - } -} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 5f0817bd269..b1ceb24647f 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,19 @@ *.classpath .vscode/ +.vscode_old/ *.project *.factorypath +# AI assistant / editor local configuration +.cursorrules +.cursor/ +.gemini/ +GEMINI.md +CLAUDE.md +AGENTS.md +.claude/ + .vertx/ target/ diff --git a/GEMINI.md b/GEMINI.md deleted file mode 100644 index f449b672b54..00000000000 --- a/GEMINI.md +++ /dev/null @@ -1,9 +0,0 @@ -## graphify - -This project has a graphify knowledge graph at graphify-out/. - -Rules: -- Before answering architecture or codebase questions, read graphify-out/GRAPH_REPORT.md for god nodes and community structure -- If graphify-out/wiki/index.md exists, navigate it instead of reading raw files -- For cross-module "how does X relate to Y" questions, prefer `graphify query ""`, `graphify path "" ""`, or `graphify explain ""` over grep — these traverse the graph's EXTRACTED + INFERRED edges instead of scanning files -- After modifying code files in this session, run `graphify update .` to keep the graph current (AST-only, no API cost) diff --git a/extra/.vscode_old/launch.json b/extra/.vscode_old/launch.json deleted file mode 100644 index 3641a02708c..00000000000 --- a/extra/.vscode_old/launch.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "java", - "name": "Debug Prebid Server (default config)", - "request": "launch", - "mainClass": "org.prebid.server.Application", - "projectName": "prebid-server-bundle", - "args": [ - "--spring.config.additional-location=sample/configs/prebid-config-with-optable.yaml" - ], - "vmArgs": "-Xmx2G -Dlogging.level.root=INFO" - }, - { - "type": "java", - "name": "Debug Prebid Server (custom YAML)", - "request": "launch", - "mainClass": "org.prebid.server.Application", - "projectName": "prebid-server", - "args": [ - "--spring.config.additional-location=config/my-prebid.yaml" - ], - "vmArgs": "-Xmx2G -Dlogging.level.root=DEBUG" - }, - { - "type": "java", - "name": "Debug Prebid Server (no args)", - "request": "launch", - "mainClass": "org.prebid.server.Application", - "projectName": "prebid-server", - "vmArgs": "-Xmx1G" - } - ] - } - \ No newline at end of file diff --git a/extra/.vscode_old/settings.json b/extra/.vscode_old/settings.json deleted file mode 100644 index 6426ceea05a..00000000000 --- a/extra/.vscode_old/settings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "java.configuration.maven.pomFile": "extra/pom.xml", - "java.import.maven.enabled": true, - "java.autobuild.enabled": true, - "java.configuration.updateBuildConfiguration": "automatic", - "java.errors.incompleteClasspath.severity": "ignore", - "files.watcherExclude": { - "**/target/**": true - }, - "java.completion.importOrder": [ - "#", - "java" - ], - "editor.codeActionsOnSave": { - "source.organizeImports": "never" - } - } - \ No newline at end of file diff --git a/extra/.vscode_old/tasks.json b/extra/.vscode_old/tasks.json deleted file mode 100644 index aa0aab32b13..00000000000 --- a/extra/.vscode_old/tasks.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "Maven: Fetch dependencies", - "type": "shell", - "command": "mvn -q dependency:go-offline", - "group": "build", - "problemMatcher": [] - }, - { - "label": "Maven: Build (full package)", - "type": "shell", - "command": "mvn -q clean package -DskipTests", - "group": "build", - "problemMatcher": "$maven" - }, - { - "label": "Maven: Test", - "type": "shell", - "command": "mvn -q test", - "group": "test", - "problemMatcher": "$maven" - }, - { - "label": "Format Java (Google Style)", - "type": "shell", - "command": "mvn -q spotless:apply", - "problemMatcher": [] - } - ] - } - \ No newline at end of file