diff --git a/extra/modules/optable-targeting/pom.xml b/extra/modules/optable-targeting/pom.xml index e202d7cfdd6..4853322dc0b 100644 --- a/extra/modules/optable-targeting/pom.xml +++ b/extra/modules/optable-targeting/pom.xml @@ -12,4 +12,12 @@ optable-targeting Optable targeting module + + + + io.vertx + vertx-junit5 + test + + diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index 91afba88a31..117b101e096 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -4,12 +4,14 @@ import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; import org.prebid.server.cache.PbcStorageService; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableRawAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingAuctionResponseHook; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingProcessedAuctionRequestHook; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.IdsMapper; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.modules.optable.targeting.v1.net.APIClientImpl; import org.prebid.server.hooks.modules.optable.targeting.v1.net.CachedAPIClient; @@ -86,18 +88,25 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties return new ConfigResolver(ObjectMapperProvider.mapper(), jsonMerger, globalProperties); } + @Bean + NetworkCall networkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) { + return new NetworkCall(optableTargeting, userFpdActivityMask); + } + @Bean OptableTargetingModule optableTargetingModule(ConfigResolver configResolver, - OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, + NetworkCall networkCall, JsonMerger jsonMerger, @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { return new OptableTargetingModule(List.of( + new OptableRawAuctionRequestHook( + configResolver, + networkCall, + logSamplingRate), new OptableTargetingProcessedAuctionRequestHook( configResolver, - optableTargeting, - userFpdActivityMask, + networkCall, logSamplingRate), new OptableTargetingAuctionResponseHook( configResolver, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index ed0264f0249..fc4122b7d48 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -1,7 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.model; +import io.vertx.core.Future; import lombok.Data; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; import java.util.List; @@ -19,8 +21,19 @@ public class ModuleContext { private long optableTargetingExecutionTime; + private boolean isEarlyNetworkCallEnabled = false; + + private Future optableTargetingCall; + + private long callTargetingAPITimestamp; + public static ModuleContext of(AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); return moduleContext != null ? moduleContext : new ModuleContext(); } + + public void failWithExecutionTime(long executionTime) { + setOptableTargetingExecutionTime(executionTime); + setEnrichRequestStatus(EnrichmentStatus.failure()); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java new file mode 100644 index 00000000000..71fd09a7c0d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableHook.java @@ -0,0 +1,37 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +public class OptableHook { + + private OptableHook() { + } + + public static boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { + return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); + } + + public static Future> update( + PayloadUpdate payloadUpdate, + ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.update) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .payloadUpdate(payloadUpdate) + .moduleContext(moduleContext) + .build()); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java new file mode 100644 index 00000000000..a96a863ba01 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHook.java @@ -0,0 +1,87 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import org.prebid.server.hooks.execution.v1.InvocationResultImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; +import org.prebid.server.hooks.v1.InvocationAction; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.InvocationStatus; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.hooks.v1.auction.RawAuctionRequestHook; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; + +import java.util.Objects; + +public class OptableRawAuctionRequestHook implements RawAuctionRequestHook { + + private static final ConditionalLogger conditionalLogger = new ConditionalLogger( + LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); + + private static final String CODE = "optable-targeting-raw-auction-request-hook"; + + private final ConfigResolver configResolver; + private final NetworkCall networkCall; + private final double logSamplingRate; + + public OptableRawAuctionRequestHook(ConfigResolver configResolver, + NetworkCall networkCall, + double logSamplingRate) { + + this.configResolver = Objects.requireNonNull(configResolver); + this.networkCall = Objects.requireNonNull(networkCall); + this.logSamplingRate = logSamplingRate; + } + + @Override + public Future> call(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext) { + + final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyNetworkCallEnabled(true); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + + if (!OptableHook.isTargetingPropertiesValid(properties)) { + conditionalLogger.error( + "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); + + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + + return OptableHook.update(BidRequestCleaner.instance(), moduleContext); + } + + final Future optableTargetingCall = networkCall.makeRequest( + payload, + invocationContext, + properties); + + moduleContext.setOptableTargetingCall(optableTargetingCall); + + return updateModuleContext(moduleContext); + } + + private static Future> updateModuleContext(ModuleContext moduleContext) { + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(AnalyticTagsResolver.toEnrichRequestAnalyticTags(moduleContext)) + .moduleContext(moduleContext) + .build()); + } + + @Override + public String code() { + return CODE; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index a5ad2559d40..80792de93ba 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -1,31 +1,16 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; -import com.iab.openrtb.request.BidRequest; -import com.iab.openrtb.request.Device; -import com.iab.openrtb.request.User; import io.vertx.core.Future; -import org.apache.commons.lang3.StringUtils; -import org.prebid.server.activity.Activity; -import org.prebid.server.activity.ComponentType; -import org.prebid.server.activity.infrastructure.ActivityInfrastructure; -import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; -import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; -import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; -import org.prebid.server.auction.model.AuctionContext; -import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; -import org.prebid.server.execution.timeout.Timeout; import org.prebid.server.hooks.execution.v1.InvocationResultImpl; import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; -import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestCleaner; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableAttributesResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -41,23 +26,22 @@ public class OptableTargetingProcessedAuctionRequestHook implements ProcessedAuctionRequestHook { private static final ConditionalLogger conditionalLogger = new ConditionalLogger( - LoggerFactory.getLogger(OptableTargetingProcessedAuctionRequestHook.class)); + LoggerFactory.getLogger(OptableRawAuctionRequestHook.class)); public static final String CODE = "optable-targeting-processed-auction-request-hook"; + private static final String AUCTION_NOT_PROPERLY_CONFIGURED = + "Account not properly configured: tenant and/or origin is missing."; + private final ConfigResolver configResolver; - private final OptableTargeting optableTargeting; - private final UserFpdActivityMask userFpdActivityMask; + private final NetworkCall networkCall; private final double logSamplingRate; public OptableTargetingProcessedAuctionRequestHook(ConfigResolver configResolver, - OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, + NetworkCall networkCall, double logSamplingRate) { - this.configResolver = Objects.requireNonNull(configResolver); - this.optableTargeting = Objects.requireNonNull(optableTargeting); - this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + this.networkCall = Objects.requireNonNull(networkCall); this.logSamplingRate = logSamplingRate; } @@ -66,80 +50,51 @@ public Future> call(AuctionRequestPayloa AuctionInvocationContext invocationContext) { final OptableTargetingProperties properties = configResolver.resolve(invocationContext.accountConfig()); - final ModuleContext moduleContext = new ModuleContext(); - final long callTargetingAPITimestamp = System.currentTimeMillis(); + final ModuleContext moduleContext = ModuleContext.of(invocationContext); - if (!isTargetingPropertiesValid(properties)) { - conditionalLogger.error( - "Account not properly configured: tenant and/or origin is missing.", logSamplingRate); + final Future optableTargetingCall = moduleContext.isEarlyNetworkCallEnabled() + ? moduleContext.getOptableTargetingCall() + : makeOptableTargetingCall(auctionRequestPayload, invocationContext, moduleContext, properties); - moduleContext.setOptableTargetingExecutionTime(System.currentTimeMillis() - callTargetingAPITimestamp); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + if (optableTargetingCall == null) { + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); return update(BidRequestCleaner.instance(), moduleContext); } - final BidRequest bidRequest = applyActivityRestrictions(auctionRequestPayload.bidRequest(), invocationContext); - - final Timeout timeout = getHookTimeout(invocationContext); - final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( - invocationContext.auctionContext(), - properties.getTimeout()); - - return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout) + final Future> future = optableTargetingCall .compose(targetingResult -> { moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - callTargetingAPITimestamp); + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); return enrichedPayload(targetingResult, moduleContext, properties); }) .recover(throwable -> { - moduleContext.setOptableTargetingExecutionTime( - System.currentTimeMillis() - callTargetingAPITimestamp); - moduleContext.setEnrichRequestStatus(EnrichmentStatus.failure()); + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); return update(BidRequestCleaner.instance(), moduleContext); }); - } - private boolean isTargetingPropertiesValid(OptableTargetingProperties properties) { - return !StringUtils.isEmpty(properties.getOrigin()) && !StringUtils.isEmpty(properties.getTenant()); + return future; } - private BidRequest applyActivityRestrictions(BidRequest bidRequest, - AuctionInvocationContext auctionInvocationContext) { - - final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); - final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( - ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), - bidRequest); - final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); - - final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( - Activity.TRANSMIT_UFPD, activityInvocationPayload); - final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( - Activity.TRANSMIT_EIDS, activityInvocationPayload); - final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( - Activity.TRANSMIT_GEO, activityInvocationPayload); - - return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); - } - - private BidRequest maskUserPersonalInfo(BidRequest bidRequest, - boolean disallowTransmitUfpd, - boolean disallowTransmitEids, - boolean disallowTransmitGeo) { - - final User maskedUser = userFpdActivityMask.maskUser( - bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); - final Device maskedDevice = userFpdActivityMask.maskDevice( - bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); - - return bidRequest.toBuilder() - .user(maskedUser) - .device(maskedDevice) - .build(); - } + private Future makeOptableTargetingCall( + AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + ModuleContext moduleContext, + OptableTargetingProperties properties) { + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis()); + if (!OptableHook.isTargetingPropertiesValid(properties)) { + conditionalLogger.error(AUCTION_NOT_PROPERLY_CONFIGURED, logSamplingRate); + + moduleContext.failWithExecutionTime( + System.currentTimeMillis() - moduleContext.getCallTargetingAPITimestamp()); + return Future.failedFuture(AUCTION_NOT_PROPERLY_CONFIGURED); + } - private Timeout getHookTimeout(AuctionInvocationContext invocationContext) { - return invocationContext.timeout(); + return networkCall.makeRequest( + payload, + invocationContext, + properties); } private Future> enrichedPayload(TargetingResult targetingResult, diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java new file mode 100644 index 00000000000..be7d2c42acb --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/NetworkCall.java @@ -0,0 +1,88 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.User; +import io.vertx.core.Future; +import org.prebid.server.activity.Activity; +import org.prebid.server.activity.ComponentType; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.activity.infrastructure.payload.ActivityInvocationPayload; +import org.prebid.server.activity.infrastructure.payload.impl.ActivityInvocationPayloadImpl; +import org.prebid.server.activity.infrastructure.payload.impl.BidRequestActivityInvocationPayload; +import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.v1.OptableTargetingModule; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import java.util.Objects; + +public class NetworkCall { + + private final OptableTargeting optableTargeting; + private final UserFpdActivityMask userFpdActivityMask; + + public NetworkCall(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask) { + + this.optableTargeting = Objects.requireNonNull(optableTargeting); + this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); + } + + public Future makeRequest(AuctionRequestPayload payload, + AuctionInvocationContext invocationContext, + OptableTargetingProperties properties) { + + final BidRequest bidRequest = applyActivityRestrictions(payload.bidRequest(), invocationContext); + + final Timeout timeout = getHookTimeout(invocationContext); + final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( + invocationContext.auctionContext(), + properties.getTimeout()); + + return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout); + } + + private static Timeout getHookTimeout(AuctionInvocationContext invocationContext) { + return invocationContext.timeout(); + } + + private BidRequest applyActivityRestrictions(BidRequest bidRequest, + AuctionInvocationContext auctionInvocationContext) { + + final AuctionContext auctionContext = auctionInvocationContext.auctionContext(); + final ActivityInvocationPayload activityInvocationPayload = BidRequestActivityInvocationPayload.of( + ActivityInvocationPayloadImpl.of(ComponentType.GENERAL_MODULE, OptableTargetingModule.CODE), + bidRequest); + final ActivityInfrastructure activityInfrastructure = auctionContext.getActivityInfrastructure(); + + final boolean disallowTransmitUfpd = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_UFPD, activityInvocationPayload); + final boolean disallowTransmitEids = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_EIDS, activityInvocationPayload); + final boolean disallowTransmitGeo = !activityInfrastructure.isAllowed( + Activity.TRANSMIT_GEO, activityInvocationPayload); + + return maskUserPersonalInfo(bidRequest, disallowTransmitUfpd, disallowTransmitEids, disallowTransmitGeo); + } + + private BidRequest maskUserPersonalInfo(BidRequest bidRequest, + boolean disallowTransmitUfpd, + boolean disallowTransmitEids, + boolean disallowTransmitGeo) { + + final User maskedUser = userFpdActivityMask.maskUser( + bidRequest.getUser(), disallowTransmitUfpd, disallowTransmitEids); + final Device maskedDevice = userFpdActivityMask.maskDevice( + bidRequest.getDevice(), disallowTransmitUfpd, disallowTransmitGeo); + + return bidRequest.toBuilder() + .user(maskedUser) + .device(maskedDevice) + .build(); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 7f2aad0657d..8009a9f2ff5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,11 +1,16 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; import org.apache.commons.collections4.SetUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; -import org.prebid.server.privacy.gdpr.model.TcfContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; import java.util.ArrayList; import java.util.List; @@ -17,18 +22,33 @@ private OptableAttributesResolver() { } public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { - final TcfContext tcfContext = auctionContext.getPrivacyContext().getTcfContext(); final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); + final BidRequest bidRequest = auctionContext.getBidRequest(); + final Optional regs = Optional.ofNullable(bidRequest.getRegs()); + final Integer gdpr = regs + .map(Regs::getGdpr) + .orElseGet(() -> regs.map(Regs::getExt) + .map(ExtRegs::getGdpr) + .orElse(null)); + final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() .ips(resolveIp(auctionContext)) .userAgent(resolveUserAgent(auctionContext)) .timeout(timeout); - if (tcfContext.isConsentValid()) { - builder - .gdprApplies(tcfContext.isInGdprScope()) - .gdprConsent(tcfContext.getConsentString()); + if (gdpr != null && gdpr > 0) { + final Optional user = Optional.ofNullable(bidRequest.getUser()); + final String consent = user.map(User::getConsent) + .orElseGet(() -> user.map(User::getExt) + .map(ExtUser::getConsent) + .orElse(null)); + + if (StringUtils.isNotEmpty(consent)) { + builder + .gdprApplies(true) + .gdprConsent(consent); + } } if (gppScope.getGppModel() != null) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 99f24ea4bc5..90f2008cec7 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -103,6 +103,14 @@ protected BidRequest givenBidRequestWithUserEids(List eids) { .build(); } + protected BidRequest givenBidRequestWithUser(User user) { + return BidRequest.builder() + .user(user) + .device(givenDevice()) + .cur(List.of("USD")) + .build(); + } + protected BidRequest givenBidRequestWithUserData(List data) { return BidRequest.builder() .user(givenUserWithData(data)) @@ -254,4 +262,8 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, protected Query givenQuery() { return Query.of("?que", "ry"); } + + protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { + return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled)); + } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java new file mode 100644 index 00000000000..00a0bbf77ee --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -0,0 +1,127 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1; + +import io.vertx.core.Future; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import lombok.SneakyThrows; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.prebid.server.activity.infrastructure.ActivityInfrastructure; +import org.prebid.server.auction.privacy.enforcement.mask.UserFpdActivityMask; +import org.prebid.server.execution.timeout.Timeout; +import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; +import org.prebid.server.hooks.v1.InvocationResult; +import org.prebid.server.hooks.v1.auction.AuctionInvocationContext; +import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.Mockito.when; + +@MockitoSettings(strictness = Strictness.LENIENT) +@ExtendWith(VertxExtension.class) +public class OptableRawAuctionRequestHookTest extends BaseOptableTest { + + @Mock + private OptableTargeting optableTargeting; + @Mock + private UserFpdActivityMask userFpdActivityMask; + @Mock + private AuctionRequestPayload auctionRequestPayload; + @Mock + private ActivityInfrastructure activityInfrastructure; + @Mock + private AuctionInvocationContext invocationContext; + @Mock + private Timeout timeout; + + private ConfigResolver configResolver; + private NetworkCall networkCall; + private OptableRawAuctionRequestHook target; + + @BeforeEach + public void setUp() { + when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) + .thenAnswer(answer -> answer.getArgument(0)); + configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); + networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, 0.01); + when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); + when(invocationContext.timeout()).thenReturn(timeout); + when(activityInfrastructure.isAllowed(any(), any())).thenReturn(true); + when(timeout.remaining()).thenReturn(1000L); + } + + @Test + public void shouldHaveRightCode() { + // when and then + assertThat(target.code()).isEqualTo("optable-targeting-raw-auction-request-hook"); + } + + @SneakyThrows + @Test + public void shouldInjectEarlyNetworkCallToModuleContext(VertxTestContext vertxTestContext) { + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", "origin", true)); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> result = + target.call(auctionRequestPayload, invocationContext); + + // then + assertThat(result).isNotNull(); + result.map(res -> (ModuleContext) res.moduleContext()) + .compose(ModuleContext::getOptableTargetingCall) + .onComplete(call -> { + vertxTestContext.verify(() -> { + assertThat(call.result()).isNotNull(); + }); + vertxTestContext.completeNow(); + }); + } + + @SneakyThrows + @Test + public void shouldNotInjectEarlyNetworkCallToModuleContextWhenOriginIsAbsentInAccountConfiguration( + VertxTestContext vertxTestContext) { + + // given + when(invocationContext.accountConfig()) + .thenReturn(givenAccountConfig("key", "tenant", null, true)); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + configResolver = new ConfigResolver( + mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, true)); + target = new OptableRawAuctionRequestHook(configResolver, networkCall, 0.01); + + // when + final Future> result = + target.call(auctionRequestPayload, invocationContext); + + // then + assertThat(result).isNotNull(); + result.map(res -> (ModuleContext) res.moduleContext()) + .onComplete(cxt -> { + vertxTestContext.verify(() -> { + final ModuleContext moduleContext = cxt.result(); + assertThat(moduleContext.getOptableTargetingCall()).isNull(); + assertThat(moduleContext.isEarlyNetworkCallEnabled()).isTrue(); + }); + vertxTestContext.completeNow(); + }); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 008262b8a3e..190004eb758 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -17,6 +17,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.NetworkCall; import org.prebid.server.hooks.modules.optable.targeting.v1.core.OptableTargeting; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -41,8 +42,6 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock private UserFpdActivityMask userFpdActivityMask; - private OptableTargetingProcessedAuctionRequestHook target; - @Mock private AuctionRequestPayload auctionRequestPayload; @@ -55,16 +54,17 @@ public class OptableTargetingProcessedAuctionRequestHookTest extends BaseOptable @Mock private Timeout timeout; + private NetworkCall networkCall; + + private OptableTargetingProcessedAuctionRequestHook target; + @BeforeEach public void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + networkCall = new NetworkCall(optableTargeting, userFpdActivityMask); + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); when(invocationContext.accountConfig()).thenReturn(givenAccountConfig(true)); when(invocationContext.auctionContext()).thenReturn(givenAuctionContext(activityInfrastructure, timeout)); @@ -131,6 +131,40 @@ public void shouldReturnResultWithUpdateActionWhenOptableTargetingReturnTargetin assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); } + @Test + public void shouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { + // given + final ModuleContext moduleContext = new ModuleContext(); + moduleContext.setEarlyNetworkCallEnabled(true); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + moduleContext.setOptableTargetingCall( + networkCall.makeRequest(auctionRequestPayload, invocationContext, givenOptableTargetingProperties( + "key", "tenant", "origin", false))); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + + final InvocationResult result = future.result(); + assertThat(result).isNotNull(); + assertThat(result.status()).isEqualTo(InvocationStatus.success); + assertThat(result.action()).isEqualTo(InvocationAction.update); + assertThat(result.errors()).isNull(); + final BidRequest bidRequest = result + .payloadUpdate() + .apply(AuctionRequestPayloadImpl.of(givenBidRequest())) + .bidRequest(); + assertThat(bidRequest.getUser().getEids().getFirst().getUids().getFirst().getId()).isEqualTo("id"); + assertThat(bidRequest.getUser().getData().getFirst().getSegment().getFirst().getId()).isEqualTo("id"); + } + @Test public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { // given @@ -138,11 +172,7 @@ public void shouldReturnFailWhenOriginIsAbsentInAccountConfiguration() { mapper, jsonMerger, givenOptableTargetingProperties("key", "tenant", null, false)); - target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", "tenant", null, true)); @@ -170,11 +200,7 @@ public void shouldReturnFailWhenTenantIsAbsentInAccountConfiguration() { mapper, jsonMerger, givenOptableTargetingProperties("key", null, "origin", false)); - target = new OptableTargetingProcessedAuctionRequestHook( - configResolver, - optableTargeting, - userFpdActivityMask, - 0.01); + target = new OptableTargetingProcessedAuctionRequestHook(configResolver, networkCall, 0.01); when(invocationContext.accountConfig()) .thenReturn(givenAccountConfig("key", null, null, true)); @@ -249,8 +275,4 @@ public void shouldReturnResultWithUpdateWhenOptableTargetingDoesntReturnResult() private ObjectNode givenAccountConfig(boolean cacheEnabled) { return givenAccountConfig("key", "tenant", "origin", cacheEnabled); } - - private ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { - return mapper.valueToTree(givenOptableTargetingProperties(key, tenant, origin, cacheEnabled)); - } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index de2c01948fb..9621758cab0 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -2,6 +2,8 @@ import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Regs; +import com.iab.openrtb.request.User; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -15,6 +17,8 @@ import org.prebid.server.privacy.gdpr.model.TcfContext; import org.prebid.server.privacy.model.Privacy; import org.prebid.server.privacy.model.PrivacyContext; +import org.prebid.server.proto.openrtb.ext.request.ExtRegs; +import org.prebid.server.proto.openrtb.ext.request.ExtUser; import java.util.List; import java.util.Set; @@ -42,15 +46,32 @@ public void setUp() { } @Test - public void shouldResolveTcfAttributesWhenConsentIsValid() { + public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() { // given final GppModel gppModel = mock(); - when(tcfContext.isConsentValid()).thenReturn(true); - when(tcfContext.isInGdprScope()).thenReturn(true); - when(tcfContext.getConsentString()).thenReturn("consent"); when(gppModel.encode()).thenReturn("consent"); when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); - final AuctionContext auctionContext = givenAuctionContext(givenBidRequest(), tcfContext, gppContext); + final AuctionContext auctionContext = + givenAuctionContext(givenBidRequestWithGdprORTB26(true, "consent"), tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(true, OptableAttributes::isGdprApplies) + .returns("consent", OptableAttributes::getGdprConsent); + } + + @Test + public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { + // given + final GppModel gppModel = mock(); + when(gppModel.encode()).thenReturn("consent"); + when(gppContext.scope()).thenReturn(GppContext.Scope.of(gppModel, Set.of(1))); + final AuctionContext auctionContext = + givenAuctionContext(givenBidRequestWithGdprORTB25(true, "consent"), tcfContext, gppContext); // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( @@ -62,6 +83,34 @@ public void shouldResolveTcfAttributesWhenConsentIsValid() { .returns("consent", OptableAttributes::getGdprConsent); } + private BidRequest givenBidRequestWithGdprORTB26(boolean isGdprEnabled, String consent) { + final User user = User.builder() + .consent(consent) + .build(); + + return BidRequest.builder() + .user(user) + .regs(Regs.builder() + .gdpr(isGdprEnabled ? 1 : 0) + .build()) + .build(); + } + + private BidRequest givenBidRequestWithGdprORTB25(boolean isGdprEnabled, String consent) { + final User user = User.builder() + .ext(ExtUser.builder() + .consent(consent) + .build()) + .build(); + + return BidRequest.builder() + .user(user) + .regs(Regs.builder() + .ext(ExtRegs.of(isGdprEnabled ? 1 : 0, null, null, null)) + .build()) + .build(); + } + @Test public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // given diff --git a/sample/configs/prebid-config-with-optable-old.yaml b/sample/configs/prebid-config-with-optable-old.yaml new file mode 100644 index 00000000000..9efe5a34e5c --- /dev/null +++ b/sample/configs/prebid-config-with-optable-old.yaml @@ -0,0 +1,53 @@ +status-response: "ok" +adapters: + appnexus: + enabled: true + ix: + enabled: true + openx: + enabled: true + pubmatic: + enabled: true + rubicon: + enabled: true + improvedigital: + enabled: true + colossus: + enabled: true + triplelift: + enabled: true +metrics: + prefix: prebid +cache: + scheme: http + host: localhost + path: /cache + query: uuid= +settings: + enforce-valid-account: false + generate-storedrequest-bidrequest-id: true + filesystem: + settings-filename: sample/configs/sample-app-settings-optable-old.yaml + stored-requests-dir: sample + stored-imps-dir: sample + stored-responses-dir: sample/stored + categories-dir: +gdpr: + default-value: 1 + vendorlist: + v2: + cache-dir: /var/tmp/vendor2 + v3: + cache-dir: /var/tmp/vendor3 +admin-endpoints: + logging-changelevel: + enabled: true + path: /logging/changelevel + on-application-port: true + protected: false +hooks: + optable-targeting: + enabled: true + modules: + optable-targeting: + api-endpoint: https://na.edge.optable.co/v2/targeting?t={{TENANT}}&o={{ORIGIN}} diff --git a/sample/configs/sample-app-settings-optable-old.yaml b/sample/configs/sample-app-settings-optable-old.yaml new file mode 100644 index 00000000000..7a533da3697 --- /dev/null +++ b/sample/configs/sample-app-settings-optable-old.yaml @@ -0,0 +1,63 @@ +accounts: + - id: 1 + status: active + auction: + price-granularity: low + privacy: + ccpa: + enabled: true + gdpr: + enabled: true + cookie-sync: + default-limit: 8 + max-limit: 15 + coop-sync: + default: true + analytics: + allow-client-details: true + hooks: + modules: + optable-targeting: + api-key: key + tenant: optable + origin: web-sdk-demo + ppid-mapping: { "pubcid.org": "c" } + adserver-targeting: true + cache: + enabled: false + ttlseconds: 86400 + execution-plan: + { + "endpoints": { + "/openrtb2/auction": { + "stages": { + "processed-auction-request": { + "groups": [ + { + "timeout": 600, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-processed-auction-request-hook" + } + ] + } + ] + }, + "auction-response": { + "groups": [ + { + "timeout": 10, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-auction-response-hook" + } + ] + } + ] + } + } + } + } + } diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 7a533da3697..571ad0a5a97 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -31,6 +31,19 @@ accounts: "endpoints": { "/openrtb2/auction": { "stages": { + "raw-auction-request": { + "groups": [ + { + "timeout": 1000, + "hook-sequence": [ + { + "module-code": "optable-targeting", + "hook-impl-code": "optable-targeting-raw-auction-request-hook" + } + ] + } + ] + }, "processed-auction-request": { "groups": [ {