diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 9e3b76c6446..586627d1288 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 above. + ## Analytics Tags The following 2 analytics tags are written by the module: 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..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 @@ -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/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/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/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index 9dacd6de322..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 @@ -23,4 +23,8 @@ public class OptableAttributes { String userAgent; Long timeout; + + App app; + + String id5Signature; } 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..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 @@ -7,9 +7,13 @@ public class Query { String ids; + String hid; + String attributes; + String hidAttributes; + public String toQueryString() { - return ids + attributes; + return ids + hid + attributes + hidAttributes; } } 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..d9cf634e273 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,7 @@ import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import lombok.NoArgsConstructor; -import org.apache.commons.collections.MapUtils; +import org.apache.commons.collections4.MapUtils; import java.util.Map; import java.util.Set; @@ -33,6 +33,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/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/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/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index 7c544a47126..c0b1bbe6325 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; @@ -61,6 +62,7 @@ private Future> enrichedPayload(Targeting if (hasData) { moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); } @@ -97,7 +99,9 @@ private static long calcExecutionTime(ModuleContext moduleContext) { return startTime > 0 ? System.currentTimeMillis() - startTime : 0; } - private Future> noAction(ModuleContext moduleContext, Tags analyticsTags) { + private Future> noAction(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 8edef6b00eb..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; @@ -13,6 +12,7 @@ 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.Id5SignatureBidResponseEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -56,21 +56,27 @@ 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) - : success(moduleContext); + ? update(fullEnrichmentChain(targeting, id5Signature), moduleContext) + : update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } - private Future> enrichedPayload(ModuleContext moduleContext) { - final List targeting = moduleContext.getTargeting(); + private PayloadUpdate fullEnrichmentChain(final List targeting, + final String id5Signature) { + + return BidResponseEnricher.of(targeting, objectMapper, jsonMerger) + .andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply; + } - return CollectionUtils.isNotEmpty(targeting) - ? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext) - : success(moduleContext); + private PayloadUpdate id5SignatureEnrichmentChain(String id5Signature) { + 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..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,8 +11,9 @@ 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.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; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -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/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/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 new file mode 100644 index 00000000000..4349ad21240 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -0,0 +1,74 @@ +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; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class Id5Resolver { + + 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() { + } + + public static String resolveId5Signature(TargetingResult targetingResult) { + if (targetingResult == null) { + return null; + } + + final List refs = Optional.of(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .map(User::getEids) + .orElseGet(List::of) + .stream() + .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(OPTABLE)) + .filter(Objects::nonNull) + .map(it -> it.get(REF)) + .filter(Objects::nonNull) + .map(JsonNode::asText) + .toList(); + + if (CollectionUtils.isEmpty(refs)) { + return null; + } + + final ObjectNode references = targetingResult.getRefs(); + if (references == null) { + return null; + } + + return refs.stream() + .map(references::get) + .filter(Objects::nonNull) + .map(it -> it.get(SIGNATURE)) + .filter(Objects::nonNull) + .filter(JsonNode::isValueNode) + .filter(it -> !it.isNull()) + .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/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/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index 7214f6ce6a3..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,8 +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; import com.iab.openrtb.request.Eid; @@ -14,8 +11,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,19 +21,14 @@ public class IdsMapper { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(IdsMapper.class)); - private static final Map STATIC_PPID_MAPPING = Map.of( "id5-sync.com", Id.ID5, "utiq.com", Id.UTIQ, "netid.de", Id.NET_ID); - private final ObjectMapper objectMapper; private final double logSamplingRate; - public IdsMapper(ObjectMapper objectMapper, double logSamplingRate) { - this.objectMapper = Objects.requireNonNull(objectMapper); + public IdsMapper(double logSamplingRate) { this.logSamplingRate = logSamplingRate; } @@ -60,7 +50,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,19 +58,15 @@ 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; 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/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 8009a9f2ff5..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 @@ -8,7 +8,9 @@ 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.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -21,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(); @@ -35,6 +40,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,15 +57,28 @@ 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())); } + Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ext -> ext.getProperty("optable")) + .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate)) + .map(ExtUserOptable::getId5Signature) + .filter(StringUtils::isNotBlank) + .ifPresent(builder::id5Signature); + 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/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..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 @@ -5,14 +5,17 @@ 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 +29,42 @@ 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), + buildHidAttributesString(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, (a, b) -> b)); + + final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) + .map(prefixToIdValue::get) + .filter(Objects::nonNull) + .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; + } + + return "&" + hidParameters; } private static String buildIdsString(List ids, String idPrefixOrder) { @@ -39,14 +72,15 @@ 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) { 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(); @@ -88,4 +122,31 @@ 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. + */ + 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/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 36cd33c7724..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,14 +29,17 @@ public class TargetingRequestExecutor { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; private final TimeoutFactory timeoutFactory; + private final 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/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..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 @@ -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), @@ -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 52143ff9fef..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 @@ -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() { @@ -273,7 +301,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/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java index 2bcce42ac94..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 @@ -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; @@ -74,6 +78,80 @@ 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 shouldNotSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() { + // 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()).isNull(); + } + + @Test + public void shouldNotSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndTargetingCallIsPresent() { + // 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()).isNull(); + } + @Test public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() { // given @@ -170,6 +248,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/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/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 7d1df51bc5e..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 @@ -1,5 +1,6 @@ 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.response.BidResponse; import io.vertx.core.Future; @@ -10,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; @@ -58,9 +60,10 @@ public void shouldHaveCode() { } @Test - public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { + public void shouldReturnResultWithUpdateActionAndWithPBSAnalyticsTags() { // given - when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); // when final Future> future = @@ -73,7 +76,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) @@ -93,7 +96,8 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)))); + 1)), + Future.succeededFuture(givenEmptyTargetingResult()))); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -127,14 +131,173 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( } @Test - public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { + public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { + // given + final String signature = "id5Signature"; + final ModuleContext moduleContext = givenModuleContext(List.of( + new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1))); + 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 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 signature = "id5Signature"; + final ModuleContext moduleContext = + givenModuleContext(null, Future.succeededFuture(givenEmptyTargetingResult())); + 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 shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargetingCallFails() { + // 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(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); + } + + @Test + public void shouldReturnUpdateActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + // 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.update, InvocationResult::action); + } + + @Test + public void shouldReturnResultWithUpdateActionWhenTargetingCallFailsAndNoBids() { + // given + 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.update, InvocationResult::action); + } + + @Test + 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)))); + 1)), + Future.failedFuture(new RuntimeException("error")))); // when final Future> future = @@ -147,10 +310,11 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(result.payloadUpdate()).isNull(); } @Test - public void shouldReturnSuccessWhenSkipEnrichmentIsTrue() { + public void shouldReturnNoActionWhenSkipEnrichmentIsTrue() { // given final ModuleContext moduleContext = givenModuleContext(); moduleContext.setShouldSkipEnrichment(true); @@ -167,6 +331,7 @@ public void shouldReturnSuccessWhenSkipEnrichmentIsTrue() { assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(result.payloadUpdate()).isNull(); } private ObjectNode givenAccountConfig(boolean 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 7387e8f85d5..ada923f273a 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); @@ -156,6 +157,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 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/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/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/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..da0e5bb4161 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -0,0 +1,339 @@ +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; +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 { + + private ObjectMapper mapper; + + @BeforeEach + 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 + 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 = 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 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 = 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("otherRef", 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 = 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() + .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 = mapper.createObjectNode(); + firstUidExt.set("optable", mapper.createObjectNode() + .set("ref", TextNode.valueOf("firstRef"))); + + final ObjectNode secondUidExt = mapper.createObjectNode(); + secondUidExt.set("optable", 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 = mapper.createObjectNode(); + refs.set("firstRef", mapper.createObjectNode() + .set("signature", TextNode.valueOf("firstSignature"))); + refs.set("secondRef", 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 shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { + // given + final ObjectNode firstUidExt = mapper.createObjectNode(); + firstUidExt.set("optable", mapper.createObjectNode() + .set("ref", TextNode.valueOf("firstRef"))); + + final ObjectNode secondUidExt = mapper.createObjectNode(); + secondUidExt.set("optable", 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 = mapper.createObjectNode(); + refs.set("secondRef", 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 = 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() + .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 = 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() + .set("signature", 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 + 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() + .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/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..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 @@ -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/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 9621758cab0..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 @@ -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; @@ -11,6 +14,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; @@ -56,7 +60,7 @@ public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -75,7 +79,7 @@ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -83,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 @@ -124,7 +100,7 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -145,7 +121,7 @@ public void shouldResolveGppAttributes() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -154,6 +130,113 @@ 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(), 0.01); + + // 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(), 0.01); + + // then + assertThat(result).isNotNull() + .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(), 0.01); + + // 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(), 0.01); + + // 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 16d5953ef71..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 @@ -1,9 +1,11 @@ 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; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import java.util.List; import java.util.Set; @@ -16,16 +18,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 +42,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 +58,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 +70,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 +86,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 +101,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 +115,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 +132,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 +152,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 +170,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 +189,331 @@ 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%3A0%3A0%3A0%3A0%3A0%3A0%3A1"); + } + + @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%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 + 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(""); + } + + @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="); + } + + @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"); + // 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 + 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 + 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) @@ -194,4 +521,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/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"); + } } diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index 0aa9851498e..4dd99c65dda 100644 --- a/sample/configs/prebid-config-with-optable.yaml +++ b/sample/configs/prebid-config-with-optable.yaml @@ -50,4 +50,4 @@ hooks: enabled: true modules: optable-targeting: - api-endpoint: https://ca.edge.optable.co/v2/targeting?t={TENANT}&o={ORIGIN} + api-endpoint: https://na.edge.optable.co/v2/targeting?t={TENANT}&o={ORIGIN} diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 4413a05769d..b05448d65ca 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -30,6 +30,7 @@ accounts: enrich-web: true enrich-app: true id-prefix-order: "e,v,c" + hid-prefixes: "c,i6" ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: