From aede90f5ec9b0c56bb7b7575ab9a3e57809fe1a3 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 23 Jul 2026 22:07:21 +0200 Subject: [PATCH 01/15] optable-targeting: add hid (resolver hint) support --- .../optable/targeting/model/Query.java | 4 +- .../config/OptableTargetingProperties.java | 5 +- .../optable/targeting/v1/core/IdsMapper.java | 5 + .../targeting/v1/core/OptableTargeting.java | 2 +- .../targeting/v1/core/QueryBuilder.java | 41 ++++++++- .../optable/targeting/v1/BaseOptableTest.java | 2 +- .../targeting/v1/core/IdsMapperTest.java | 14 ++- .../targeting/v1/core/QueryBuilderTest.java | 92 ++++++++++++++++--- .../configs/sample-app-settings-optable.yaml | 3 +- 9 files changed, 148 insertions(+), 20 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java index 20862050f39..19b725ab666 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -7,9 +7,11 @@ public class Query { String ids; + String hid; + String attributes; public String toQueryString() { - return ids + attributes; + return ids + hid + attributes; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 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/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index 7214f6ce6a3..a9b4c9c7311 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -81,6 +81,11 @@ private static void addDeviceIds(Map ids, Device device) { final String ifa = device != null ? device.getIfa() : null; final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null; final int lmt = Optional.ofNullable(device).map(Device::getLmt).orElse(0); + final String ip6 = device != null ? device.getIpv6() : null; + + if (ip6 != null) { + ids.put(Id.DEVICE_IP_V_6, ip6); + } if (ifa == null || StringUtils.isEmpty(os) || lmt == 1) { return; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java index 0cb16d9c456..b79ca2790f2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargeting.java @@ -29,7 +29,7 @@ public Future getTargeting(OptableTargetingProperties propertie Timeout timeout) { final List ids = idsMapper.toIds(bidRequest, properties.getPpidMapping()); - final Query query = QueryBuilder.build(ids, attributes, properties.getIdPrefixOrder()); + final Query query = QueryBuilder.build(ids, attributes, properties); if (query == null) { return Future.failedFuture("Can't get targeting"); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index bee89818b2f..63f4990f14c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -1,18 +1,22 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; import java.util.function.Predicate; @@ -26,12 +30,40 @@ public class QueryBuilder { private QueryBuilder() { } - public static Query build(List ids, OptableAttributes optableAttributes, String idPrefixOrder) { + public static Query build(List ids, OptableAttributes optableAttributes, + OptableTargetingProperties properties) { + if (CollectionUtils.isEmpty(ids) && CollectionUtils.isEmpty(optableAttributes.getIps())) { return null; } - return Query.of(buildIdsString(ids, idPrefixOrder), buildAttributesString(optableAttributes)); + final String idPrefixOrder = properties.getIdPrefixOrder(); + final String hidPrefixes = properties.getHidPrefixes(); + return Query.of( + buildIdsString(ids, idPrefixOrder), + buildHidString(ids, hidPrefixes), + buildAttributesString(optableAttributes)); + } + + private static String buildHidString(List ids, String hidPrefixesString) { + if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(hidPrefixesString)) { + return StringUtils.EMPTY; + } + final Map prefixToIdValue = ids.stream().collect(Collectors.toMap(Id::getName, it -> it)); + if (MapUtils.isEmpty(prefixToIdValue)) { + return StringUtils.EMPTY; + } + + final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) + .map(prefixToIdValue::get) + .filter(Objects::nonNull) + .map(it -> String.format("hid=%s:%s", it.getName(), it.getValue())) + .collect(Collectors.joining("&")); + if (StringUtils.isEmpty(hidParameters)) { + return StringUtils.EMPTY; + } + + return "&" + hidParameters; } private static String buildIdsString(List ids, String idPrefixOrder) { @@ -39,7 +71,10 @@ private static String buildIdsString(List ids, String idPrefixOrder) { return StringUtils.EMPTY; } - final List reorderedIds = reorderIds(ids, idPrefixOrder); + final List reorderedIds = reorderIds(ids, idPrefixOrder) + .stream() + .filter(id -> !Id.DEVICE_IP_V_6.equals(id.getName())) + .toList(); final StringBuilder sb = new StringBuilder(); for (Id id : reorderedIds) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 52143ff9fef..4bfb1947b5f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -273,7 +273,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, } protected Query givenQuery() { - return Query.of("?que", "ry"); + return Query.of("?que", "r", "y"); } protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java index 39693661629..714fc538a34 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapperTest.java @@ -56,7 +56,8 @@ public void shouldMapBidRequestToAllPossibleIds() { .doesNotContain(Id.of(Id.APPLE_IDFA, "ifa")) .contains(Id.of(Id.ID5, "id5_id")) .contains(Id.of(Id.UTIQ, "utiq_id")) - .contains(Id.of("c", "test_id")); + .contains(Id.of("c", "test_id")) + .contains(Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); } @Test @@ -71,6 +72,17 @@ public void shouldMapNothing() { assertThat(ids).isNotNull(); } + @Test + public void shouldMapIpv6WhenPresent() { + final BidRequest bidRequest = givenBidRequestWithEids(Map.of()); + + final List ids = target.toIds(bidRequest, Map.of()); + + assertThat(ids).isNotNull() + .contains(Id.of(Id.EMAIL, "email")) + .contains(Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); + } + private BidRequest givenBidRequestWithEids(Map eids) { final JsonNode extUserOptable = objectMapper.convertValue(givenOptable(), JsonNode.class); final ExtUser extUser = ExtUser.builder().build(); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 16d5953ef71..9be6fc3a2b6 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -4,6 +4,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import java.util.List; import java.util.Set; @@ -16,16 +17,21 @@ public class QueryBuilderTest { private final String idPrefixOrder = "c,c1"; + private OptableTargetingProperties properties() { + return givenProperties(idPrefixOrder, null); + } + @Test public void shouldSeparateAttributesFromIds() { // given final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, optableAttributes, properties()); // then assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); + assertThat(query.getHid()).isEqualTo(""); assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); } @@ -35,10 +41,11 @@ public void shouldBuildFullQueryString() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final Query query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, optableAttributes, properties()); // then assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); + assertThat(query.getHid()).isEqualTo(""); assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); assertThat(query.toQueryString()) .isEqualTo("&id=e%3Aemail&id=p%3A123&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); @@ -50,7 +57,7 @@ public void shouldBuildQueryStringWhenHaveIds() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString(); // then assertThat(query).contains("e%3Aemail", "p%3A123"); @@ -62,7 +69,7 @@ public void shouldBuildQueryStringWithExtraAttributes() { final List ids = List.of(Id.of(Id.EMAIL, "email"), Id.of(Id.PHONE, "123")); // when - final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString(); // then assertThat(query).contains("&gdpr=1", "&gdpr_consent=tcf", "&timeout=100ms"); @@ -78,7 +85,7 @@ public void shouldBuildQueryStringWithRightOrder() { Id.of("c", "234")); // when - final String query = QueryBuilder.build(ids, optableAttributes, idPrefixOrder).toQueryString(); + final String query = QueryBuilder.build(ids, optableAttributes, properties()).toQueryString(); // then assertThat(query).startsWith("&id=c%3A234&id=c1%3A123&id=id5%3AID5&id=e%3Aemail"); @@ -93,7 +100,7 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { .build(); // when - final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, attributes, properties()); // then assertThat(query).isNotNull(); @@ -107,7 +114,7 @@ public void shouldNotBuildQueryStringWhenIdsListIsEmptyAndIpIsAbsent() { final OptableAttributes attributes = OptableAttributes.builder().build(); // when - final Query query = QueryBuilder.build(ids, attributes, idPrefixOrder); + final Query query = QueryBuilder.build(ids, attributes, properties()); // then assertThat(query).isNull(); @@ -124,7 +131,7 @@ public void shouldBuildQueryStringWithGppSid() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then assertThat(query).contains("&gpp=DBABzw~1YNY~BVQqAAAAAgA"); @@ -144,7 +151,7 @@ public void shouldBuildQueryStringWithSingleGppSid() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then assertThat(query).contains("&gpp_sid=7"); @@ -162,7 +169,7 @@ public void shouldLimitGppSidToTwoValues() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then final String gppSidValue = query.split("gpp_sid=")[1].split("&")[0]; @@ -181,12 +188,68 @@ public void shouldNotIncludeGppSidWhenEmpty() { .build(); // when - final String query = QueryBuilder.build(ids, attributes, null).toQueryString(); + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); // then assertThat(query).doesNotContain("gpp_sid"); } + @Test + public void shouldBuildHidWhenHidPrefixesMatchIds() { + // given + final OptableTargetingProperties props = givenProperties(null, "c,i6"); + final List ids = List.of( + Id.of("c", "234"), + Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0:0:0:0:0:0:0:1"); + } + + @Test + public void shouldExcludeDeviceIpV6FromIdsString() { + // given + final OptableTargetingProperties props = givenProperties(null, "i6"); + final List ids = List.of( + Id.of(Id.EMAIL, "email"), + Id.of(Id.DEVICE_IP_V_6, "0:0:0:0:0:0:0:1")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getIds()).doesNotContain(Id.DEVICE_IP_V_6); + assertThat(query.getHid()).isEqualTo("&hid=i6:0:0:0:0:0:0:0:1"); + } + + @Test + public void shouldNotBuildHidWhenNoMatch() { + // given + final OptableTargetingProperties props = givenProperties(null, "nonexistent"); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo(""); + } + + @Test + public void shouldNotBuildHidWhenHidPrefixesNotConfigured() { + // given + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, properties()); + + // then + assertThat(query.getHid()).isEqualTo(""); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) @@ -194,4 +257,11 @@ private OptableAttributes givenOptableAttributes() { .gdprConsent("tcf") .build(); } + + private static OptableTargetingProperties givenProperties(String idPrefixOrder, String hidPrefixes) { + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setIdPrefixOrder(idPrefixOrder); + properties.setHidPrefixes(hidPrefixes); + return properties; + } } diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 4413a05769d..7b4af19a7e2 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -30,7 +30,8 @@ accounts: enrich-web: true enrich-app: true id-prefix-order: "e,v,c" - ppid-mapping: { "pubcid.org": "c" } + hid-prefixes: "c,i6" + ppid-mapping: { "pubmatic.com": "c" } adserver-targeting: true cache: enabled: false From 81c88102c1d89384d3f9e992e5a7537b0054d696 Mon Sep 17 00:00:00 2001 From: softcoder Date: Thu, 23 Jul 2026 23:04:43 +0200 Subject: [PATCH 02/15] optable-targeting: add app bundle and ver to the Targeting API call --- .../modules/optable/targeting/model/App.java | 11 ++++ .../targeting/model/OptableAttributes.java | 2 + .../v1/core/OptableAttributesResolver.java | 9 ++- .../targeting/v1/core/QueryBuilder.java | 13 ++++ .../core/OptableAttributesResolverTest.java | 37 ++++++++++++ .../targeting/v1/core/QueryBuilderTest.java | 60 +++++++++++++++++++ 6 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java new file mode 100644 index 00000000000..1b1cdb51c63 --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/App.java @@ -0,0 +1,11 @@ +package org.prebid.server.hooks.modules.optable.targeting.model; + +import lombok.Value; + +@Value(staticConstructor = "of") +public class App { + + String bundle; + + String ver; +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index 9dacd6de322..eda2ca74071 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -23,4 +23,6 @@ public class OptableAttributes { String userAgent; Long timeout; + + App app; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 8009a9f2ff5..928d2a9f808 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -8,6 +8,7 @@ import org.apache.commons.lang3.StringUtils; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -35,6 +36,7 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() .ips(resolveIp(auctionContext)) .userAgent(resolveUserAgent(auctionContext)) + .app(resolveApp(auctionContext)) .timeout(timeout); if (gdpr != null && gdpr > 0) { @@ -51,7 +53,7 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, } } - if (gppScope.getGppModel() != null) { + if (gppScope != null && gppScope.getGppModel() != null) { builder .gpp(gppScope.getGppModel().encode()) .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); @@ -60,6 +62,11 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, return builder.build(); } + private static App resolveApp(AuctionContext auctionContext) { + final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp(); + return app != null ? App.of(app.getBundle(), app.getVer()) : null; + } + public static String resolveUserAgent(AuctionContext auctionContext) { final Device device = auctionContext.getBidRequest().getDevice(); return device != null ? device.getUa() : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 63f4990f14c..ecd62f7a939 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -121,6 +121,19 @@ private static String buildAttributesString(OptableAttributes optableAttributes) sb.append("&osdk=").append(REQUEST_SOURCE); + Optional.ofNullable(optableAttributes.getApp()) + .ifPresent(app -> { + final String bundle = app.getBundle(); + if (StringUtils.isNotEmpty(bundle)) { + sb.append("&bundle=").append(bundle); + + final String ver = app.getVer(); + if (StringUtils.isNotEmpty(ver)) { + sb.append("&ver=").append(ver); + } + } + }); + return sb.toString(); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 9621758cab0..38a1cb3520a 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -11,6 +11,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.auction.gpp.model.GppContext; import org.prebid.server.auction.model.AuctionContext; +import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; @@ -154,6 +155,42 @@ public void shouldResolveGppAttributes() { .returns(Set.of(1), OptableAttributes::getGppSid); } + @Test + public void shouldResolveAppWhenAppIsPresent() { + // given + final com.iab.openrtb.request.App ortbApp = com.iab.openrtb.request.App.builder() + .bundle("com.example.app") + .ver("1.2.3") + .build(); + final BidRequest bidRequest = BidRequest.builder() + .app(ortbApp) + .build(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(App.of("com.example.app", "1.2.3"), OptableAttributes::getApp); + } + + @Test + public void shouldNotResolveAppWhenAppIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder().build(); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(null, OptableAttributes::getApp); + } + public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { return AuctionContext.builder() .bidRequest(bidRequest) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 9be6fc3a2b6..25d9992110e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -1,6 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.Query; @@ -250,6 +251,65 @@ public void shouldNotBuildHidWhenHidPrefixesNotConfigured() { assertThat(query.getHid()).isEqualTo(""); } + @Test + public void shouldAppendBundleAndVerWhenAppHasBoth() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).contains("&bundle=com.example.app", "&ver=1.2.3"); + } + + @Test + public void shouldAppendBundleOnlyWhenVerIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).contains("&bundle=com.example.app"); + assertThat(query).doesNotContain("&ver="); + } + + @Test + public void shouldNotAppendBundleAndVerWhenBundleIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).doesNotContain("&bundle=", "&ver="); + } + + @Test + public void shouldNotAppendBundleAndVerWhenAppIsNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).doesNotContain("&bundle=", "&ver="); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) From a66497eaf00631febb1c9c45fb504f0f7743a48f Mon Sep 17 00:00:00 2001 From: softcoder Date: Sat, 25 Jul 2026 00:16:25 +0200 Subject: [PATCH 03/15] optable-targeting: Add id5_signature propagation from client to Targeting API --- .../targeting/model/OptableAttributes.java | 2 + .../model/openrtb/ExtUserOptable.java | 2 + .../targeting/v1/core/BidRequestCleaner.java | 2 +- .../v1/core/OptableAttributesResolver.java | 19 ++++ .../targeting/v1/core/QueryBuilder.java | 3 + .../v1/core/BidRequestCleanerTest.java | 4 +- .../core/OptableAttributesResolverTest.java | 102 +++++++++++++----- .../targeting/v1/core/QueryBuilderTest.java | 28 +++++ 8 files changed, 132 insertions(+), 30 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java index eda2ca74071..d53d1963795 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/OptableAttributes.java @@ -25,4 +25,6 @@ public class OptableAttributes { Long timeout; App app; + + String id5Signature; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java index 787b3adbf1a..a0c8f974b69 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/ExtUserOptable.java @@ -17,4 +17,6 @@ public class ExtUserOptable extends FlexibleExtension { String zip; String vid; + + String id5Signature; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java index 30652383942..d467e6df750 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleaner.java @@ -14,7 +14,7 @@ public class BidRequestCleaner implements PayloadUpdate { private static final String OPTABLE_FIELD = "optable"; - private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid"); + private static final List FIELDS_TO_REMOVE = List.of("email", "phone", "zip", "vid", "id5_signature"); public static BidRequestCleaner instance() { return new BidRequestCleaner(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 928d2a9f808..0122fca451d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,5 +1,7 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Regs; @@ -10,6 +12,8 @@ import org.prebid.server.auction.model.AuctionContext; import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -59,9 +63,24 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); } + final Optional extUserOptable = Optional.ofNullable(bidRequest.getUser()) + .map(User::getExt) + .map(ext -> ext.getProperty("optable")) + .map(OptableAttributesResolver::parseExtUserOptable); + + extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); + return builder.build(); } + private static ExtUserOptable parseExtUserOptable(JsonNode node) { + try { + return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class); + } catch (JsonProcessingException e) { + return null; + } + } + private static App resolveApp(AuctionContext auctionContext) { final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp(); return app != null ? App.of(app.getBundle(), app.getVer()) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index ecd62f7a939..00aca36e8f8 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -134,6 +134,9 @@ private static String buildAttributesString(OptableAttributes optableAttributes) } }); + Optional.ofNullable(optableAttributes.getId5Signature()) + .ifPresent(id5Signature -> sb.append("&id5_signature=").append(id5Signature)); + return sb.toString(); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java index 6aa4ebff3a8..606c89ee102 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestCleanerTest.java @@ -33,7 +33,8 @@ public void shouldKeepOtherUserExtOptableTags() { // given final User user = givenUser(); ((com.fasterxml.jackson.databind.node.ObjectNode) user.getExt().getProperty("optable")) - .put("other", "value"); + .put("other", "value") + .put("id5_signature", "signature"); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest(bidRequest -> bidRequest.user(user))); @@ -50,6 +51,7 @@ public void shouldKeepOtherUserExtOptableTags() { .satisfies(optable -> { assertThat(optable.has("other")).isTrue(); assertThat(optable.has("email")).isFalse(); + assertThat(optable.has("id5_signature")).isFalse(); }); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 38a1cb3520a..276c3f30c88 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -1,9 +1,12 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; import com.iab.gpp.encoder.GppModel; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Regs; import com.iab.openrtb.request.User; +import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -84,34 +87,6 @@ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { .returns("consent", OptableAttributes::getGdprConsent); } - private BidRequest givenBidRequestWithGdprORTB26(boolean isGdprEnabled, String consent) { - final User user = User.builder() - .consent(consent) - .build(); - - return BidRequest.builder() - .user(user) - .regs(Regs.builder() - .gdpr(isGdprEnabled ? 1 : 0) - .build()) - .build(); - } - - private BidRequest givenBidRequestWithGdprORTB25(boolean isGdprEnabled, String consent) { - final User user = User.builder() - .ext(ExtUser.builder() - .consent(consent) - .build()) - .build(); - - return BidRequest.builder() - .user(user) - .regs(Regs.builder() - .ext(ExtRegs.of(isGdprEnabled ? 1 : 0, null, null, null)) - .build()) - .build(); - } - @Test public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // given @@ -191,6 +166,77 @@ public void shouldNotResolveAppWhenAppIsAbsent() { .returns(null, OptableAttributes::getApp); } + @Test + public void shouldResolveId5SignatureWhenPresentInUserExtOptable() { + // given + final BidRequest bidRequest = givenBidRequestWithId5Signature("signature"); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns("signature", OptableAttributes::getId5Signature); + } + + @Test + public void shouldNotResolveId5SignatureWhenAbsentInUserExtOptable() { + // given + final BidRequest bidRequest = givenBidRequestWithId5Signature(null); + final AuctionContext auctionContext = givenAuctionContext(bidRequest, tcfContext, gppContext); + + // when + final OptableAttributes result = OptableAttributesResolver.resolveAttributes( + auctionContext, properties.getTimeout()); + + // then + assertThat(result).isNotNull() + .returns(null, OptableAttributes::getId5Signature); + } + + private BidRequest givenBidRequestWithId5Signature(String signature) { + final ObjectNode optable = mapper.createObjectNode(); + if (StringUtils.isNotEmpty(signature)) { + optable.set("id5_signature", TextNode.valueOf(signature)); + } + + final ExtUser extUser = ExtUser.builder().build(); + extUser.addProperty("optable", optable); + final User user = User.builder().ext(extUser).build(); + + return BidRequest.builder().user(user).build(); + } + + private BidRequest givenBidRequestWithGdprORTB26(boolean isGdprEnabled, String consent) { + final User user = User.builder() + .consent(consent) + .build(); + + return BidRequest.builder() + .user(user) + .regs(Regs.builder() + .gdpr(isGdprEnabled ? 1 : 0) + .build()) + .build(); + } + + private BidRequest givenBidRequestWithGdprORTB25(boolean isGdprEnabled, String consent) { + final User user = User.builder() + .ext(ExtUser.builder() + .consent(consent) + .build()) + .build(); + + return BidRequest.builder() + .user(user) + .regs(Regs.builder() + .ext(ExtRegs.of(isGdprEnabled ? 1 : 0, null, null, null)) + .build()) + .build(); + } + public AuctionContext givenAuctionContext(BidRequest bidRequest, TcfContext tcfContext, GppContext gppContext) { return AuctionContext.builder() .bidRequest(bidRequest) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 25d9992110e..64aff679be4 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -310,6 +310,34 @@ public void shouldNotAppendBundleAndVerWhenAppIsNull() { assertThat(query).doesNotContain("&bundle=", "&ver="); } + @Test + public void shouldAppendId5SignatureWhenPresent() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .id5Signature("signature") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).contains("&id5_signature=signature"); + } + + @Test + public void shouldNotAppendId5SignatureWhenNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String query = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(query).doesNotContain("&id5_signature="); + } + private OptableAttributes givenOptableAttributes() { return OptableAttributes.builder() .timeout(100L) From 8902c11092182591dae25aa137c39ac3f5e218e3 Mon Sep 17 00:00:00 2001 From: softcoder Date: Sun, 26 Jul 2026 12:35:44 +0200 Subject: [PATCH 04/15] optable-targeting: Add id5_signature propagation from Targeting API to client --- .../model/openrtb/TargetingResult.java | 3 + .../OptableTargetingAuctionResponseHook.java | 38 ++- ...eTargetingProcessedAuctionRequestHook.java | 2 +- .../targeting/v1/core/Id5Resolver.java | 52 +++++ .../core/Id5SignatureBidResponseEnricher.java | 83 +++++++ .../targeting/v1/net/CachedAPIClient.java | 2 +- .../optable/targeting/v1/BaseOptableTest.java | 32 ++- ...tableTargetingAuctionResponseHookTest.java | 136 ++++++++++- .../optable/targeting/v1/core/CacheTest.java | 3 +- .../targeting/v1/core/Id5ResolverTest.java | 217 ++++++++++++++++++ .../Id5SignatureBidResponseEnricherTest.java | 122 ++++++++++ .../configs/prebid-config-with-optable.yaml | 2 +- 12 files changed, 679 insertions(+), 13 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java index 826ce2698ed..5bc11a92bd3 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/openrtb/TargetingResult.java @@ -1,5 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.model.openrtb; +import com.fasterxml.jackson.databind.node.ObjectNode; import lombok.Value; import java.util.List; @@ -10,4 +11,6 @@ public class TargetingResult { List audience; Ortb2 ortb2; + + ObjectNode refs; } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 8edef6b00eb..a3f0467d7c9 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -9,10 +9,13 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5SignatureBidResponseEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; @@ -62,15 +65,42 @@ public Future> call(AuctionResponsePayl return validationStatus.getStatus() == Status.SUCCESS ? enrichedPayload(moduleContext) - : success(moduleContext); + : enrichedById5SignaturePayload(moduleContext); } private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); - return CollectionUtils.isNotEmpty(targeting) - ? update(BidResponseEnricher.of(targeting, objectMapper, jsonMerger), moduleContext) - : success(moduleContext); + return moduleContext.getOptableTargetingCall() + .compose(targetingResult -> + CollectionUtils.isNotEmpty(targeting) + ? update(fullEnrichmentChain(moduleContext, targeting, targetingResult), moduleContext) + : update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) + .recover(throwable -> success(moduleContext)); + } + + private Future> enrichedById5SignaturePayload( + ModuleContext moduleContext) { + + return moduleContext.getOptableTargetingCall() + .compose(targetingResult -> + update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) + .recover(throwable -> success(moduleContext)); + } + + private PayloadUpdate fullEnrichmentChain(ModuleContext moduleContext, + final List targeting, + TargetingResult targetingResult) { + final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); + + return BidResponseEnricher.of(targeting, objectMapper, jsonMerger) + .andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply; + } + + private PayloadUpdate id5SignatureEnrichmentChain(TargetingResult targetingResult) { + final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); + + return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger); } private Future> update( diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index dfa8520f157..384e5c21f1b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -11,8 +11,8 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; import org.prebid.server.hooks.v1.InvocationStatus; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java new file mode 100644 index 00000000000..d7a17c1365a --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -0,0 +1,52 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +public class Id5Resolver { + + private Id5Resolver() { + + } + + public static String resolveId5Signature(TargetingResult targetingResult) { + if (targetingResult == null) { + return null; + } + + final String ref = Optional.of(targetingResult) + .map(TargetingResult::getOrtb2) + .map(Ortb2::getUser) + .map(User::getEids) + .orElseGet(List::of) + .stream() + .filter(it -> "optable.co".equals(it.getInserter()) && "id5-sync.com".equals(it.getSource())) + .map(Eid::getUids) + .flatMap(Collection::stream) + .map(Uid::getExt) + .map(it -> it.at("/optable/ref")) + .filter(Objects::nonNull) + .map(JsonNode::asText) + .findFirst() + .orElse(null); + + if (ref == null) { + return null; + } + + return Optional.ofNullable(targetingResult.getRefs()) + .map(refs -> refs.get(ref)) + .map(it -> it.get("signature")) + .map(JsonNode::asText) + .orElse(null); + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java new file mode 100644 index 00000000000..e94af71928d --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricher.java @@ -0,0 +1,83 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.response.BidResponse; +import org.apache.commons.lang3.StringUtils; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.v1.PayloadUpdate; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; + +import java.util.Objects; + +public class Id5SignatureBidResponseEnricher implements PayloadUpdate { + + private final String id5Signature; + private final ObjectMapper mapper; + private final JsonMerger jsonMerger; + + private Id5SignatureBidResponseEnricher(String id5Signature, ObjectMapper mapper, JsonMerger jsonMerger) { + this.id5Signature = id5Signature; + this.mapper = Objects.requireNonNull(mapper); + this.jsonMerger = Objects.requireNonNull(jsonMerger); + } + + public static Id5SignatureBidResponseEnricher of(String id5Signature, ObjectMapper mapper, JsonMerger jsonMerger) { + return new Id5SignatureBidResponseEnricher(id5Signature, mapper, jsonMerger); + } + + @Override + public AuctionResponsePayload apply(AuctionResponsePayload payload) { + return AuctionResponsePayloadImpl.of(enrichBidResponse(payload.bidResponse(), id5Signature)); + } + + private BidResponse enrichBidResponse(BidResponse bidResponse, String id5Signature) { + if (StringUtils.isEmpty(id5Signature)) { + return bidResponse; + } + final ObjectNode passthroughNode = id5SignatureToObjectNode(id5Signature); + + final ExtBidResponse existingExt = bidResponse.getExt(); + final ExtBidResponsePrebid existingPrebid = existingExt != null ? existingExt.getPrebid() : null; + final JsonNode existingPassthrough = existingPrebid != null ? existingPrebid.getPassthrough() : null; + + final JsonNode mergedPassthrough = existingPassthrough != null + ? jsonMerger.merge(passthroughNode, existingPassthrough) + : passthroughNode; + + final ExtBidResponsePrebid modifiedPrebid = existingPrebid != null + ? existingPrebid.toBuilder() + .passthrough(mergedPassthrough) + .build() + : ExtBidResponsePrebid.builder() + .passthrough(mergedPassthrough) + .build(); + + final ExtBidResponse modifiedExt = existingExt != null + ? existingExt.toBuilder() + .prebid(modifiedPrebid) + .build() + : ExtBidResponse.builder() + .prebid(modifiedPrebid) + .build(); + + return bidResponse.toBuilder() + .ext(modifiedExt) + .build(); + } + + private ObjectNode id5SignatureToObjectNode(String id5Signature) { + final ObjectNode node = mapper.createObjectNode(); + node.set("id5_signature", TextNode.valueOf(id5Signature)); + + final ObjectNode optableNode = mapper.createObjectNode(); + optableNode.set("optable", node); + + return optableNode; + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index e7e8bc3e452..eabccfdbff0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -42,7 +42,7 @@ public Future getTargeting(OptableTargetingProperties propertie return cache.get(createCachingKey(tenant, origin, ips, query, true)) .recover(ignore -> apiClient.getTargeting(properties, query, ips, userAgent, timeout) .recover(throwable -> isCircuitBreakerEnabled - ? Future.succeededFuture(new TargetingResult(null, null)) + ? Future.succeededFuture(new TargetingResult(null, null, null)) : Future.failedFuture(throwable)) .compose(result -> cache.put( createCachingKey(tenant, origin, ips, query, false), diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 4bfb1947b5f..2ebf65c7b6e 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -17,6 +17,7 @@ import com.iab.openrtb.response.BidResponse; import com.iab.openrtb.response.SeatBid; import io.netty.handler.codec.http.HttpResponseStatus; +import io.vertx.core.Future; import io.vertx.core.MultiMap; import io.vertx.core.http.HttpHeaders; import org.apache.commons.io.IOUtils; @@ -73,6 +74,28 @@ protected ModuleContext givenModuleContext(List audiences) { return moduleContext; } + protected ModuleContext givenModuleContext(List audiences, Future optableTargetingCall) { + final ModuleContext moduleContext = givenModuleContext(audiences); + moduleContext.setOptableTargetingCall(optableTargetingCall); + return moduleContext; + } + + protected Eid givenId5Eid(String refValue) { + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode().set("ref", TextNode.valueOf(refValue))); + return Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + } + + protected ObjectNode givenRefsObject(String refValue, String signature) { + final ObjectNode refs = mapper.createObjectNode(); + refs.set(refValue, mapper.createObjectNode().set("signature", TextNode.valueOf(signature))); + return refs; + } + protected AuctionContext givenAuctionContext(ActivityInfrastructure activityInfrastructure, Timeout timeout, Account account) { @@ -168,17 +191,22 @@ protected TargetingResult givenTargetingResult() { } protected TargetingResult givenTargetingResult(List eids, List data) { + return givenTargetingResult(eids, data, null); + } + + protected TargetingResult givenTargetingResult(List eids, List data, ObjectNode refs) { return new TargetingResult( List.of(new Audience( "provider", List.of(new AudienceId("id")), "keyspace", 1)), - new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data))); + new Ortb2(new org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User(eids, data)), + refs); } protected TargetingResult givenEmptyTargetingResult() { - return new TargetingResult(Collections.emptyList(), new Ortb2(null)); + return new TargetingResult(Collections.emptyList(), new Ortb2(null), null); } protected User givenUser() { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 7d1df51bc5e..29429910f8d 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -1,6 +1,8 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; +import com.iab.openrtb.request.Eid; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; @@ -12,6 +14,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -60,7 +63,8 @@ public void shouldHaveCode() { @Test public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { // given - when(invocationContext.moduleContext()).thenReturn(givenModuleContext()); + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); // when final Future> future = @@ -93,7 +97,8 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)))); + 1)), + Future.succeededFuture(givenEmptyTargetingResult()))); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -126,6 +131,130 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); } + @Test + public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + when(invocationContext.moduleContext()).thenReturn(givenModuleContext( + List.of(new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.succeededFuture(targetingResult))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + + @Test + public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.succeededFuture(targetingResult))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); + } + + @Test + public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsPresent() { + // given + when(invocationContext.moduleContext()).thenReturn(givenModuleContext( + List.of(new Audience( + "provider", + List.of(new AudienceId("audienceId")), + "keyspace", + 1)), + Future.failedFuture(new RuntimeException("error")))); + when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + } + + @Test + public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + // given + when(invocationContext.moduleContext()).thenReturn( + givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); + + // when + final Future> future = + target.call(auctionResponsePayload, invocationContext); + final InvocationResult result = future.result(); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + } + @Test public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { // given @@ -134,7 +263,8 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)))); + 1)), + Future.failedFuture(new RuntimeException("error")))); // when final Future> future = diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java index 1917fd6ca27..e0a723bbfa3 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/CacheTest.java @@ -109,6 +109,7 @@ private TargetingResult givenTargetingResult() { List.of(new AudienceId("1")), "keyspace", 0)), - new Ortb2(new User(null, null))); + new Ortb2(new User(null, null)), + null); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java new file mode 100644 index 00000000000..b0b9be37b21 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -0,0 +1,217 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.request.Eid; +import com.iab.openrtb.request.Uid; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; +import org.prebid.server.json.ObjectMapperProvider; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Id5ResolverTest { + + @Test + public void shouldReturnNullWhenTargetingResultIsNull() { + // when + final String result = Id5Resolver.resolveId5Signature(null); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenOrtb2IsNull() { + // given + final TargetingResult targetingResult = new TargetingResult(List.of(), null, null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenUserHasNoEids() { + // given + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(null, null)), + null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenEidsDoNotMatchOptableAndId5Source() { + // given + final Eid eid = Eid.builder() + .source("other-source") + .inserter("other-inserter") + .uids(List.of(Uid.builder().id("id").build())) + .build(); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + null); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("otherRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("signatureValue"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnSignatureWhenAllConditionsAreMet() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("signatureValue"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isEqualTo("signatureValue"); + } + + @Test + public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { + // given + final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); + firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("firstRef"))); + + final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); + secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("secondRef"))); + + final Eid firstEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id1").ext(firstUidExt).build())) + .build(); + final Eid secondEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("firstRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("firstSignature"))); + refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("secondSignature"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(firstEid, secondEid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isEqualTo("firstSignature"); + } + + @Test + public void shouldReturnNullWhenRefEntryHasNoSignature() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("other", TextNode.valueOf("value"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java new file mode 100644 index 00000000000..d9eb8bd1798 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5SignatureBidResponseEnricherTest.java @@ -0,0 +1,122 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import com.iab.openrtb.response.BidResponse; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.execution.v1.auction.AuctionResponsePayloadImpl; +import org.prebid.server.hooks.v1.auction.AuctionResponsePayload; +import org.prebid.server.json.JacksonMapper; +import org.prebid.server.json.JsonMerger; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponse; +import org.prebid.server.proto.openrtb.ext.response.ExtBidResponsePrebid; + +import static org.assertj.core.api.Assertions.assertThat; + +public class Id5SignatureBidResponseEnricherTest { + + private final JacksonMapper jacksonMapper = new JacksonMapper(ObjectMapperProvider.mapper()); + private final JsonMerger jsonMerger = new JsonMerger(jacksonMapper); + + @Test + public void shouldReturnOriginBidResponseWhenId5SignatureIsNull() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of(null, ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + assertThat(result.bidResponse()).isSameAs(bidResponse); + } + + @Test + public void shouldReturnOriginBidResponseWhenId5SignatureIsEmpty() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + assertThat(result.bidResponse()).isSameAs(bidResponse); + } + + @Test + public void shouldAddId5SignatureToPassthroughWhenExtIsAbsent() { + // given + final BidResponse bidResponse = BidResponse.builder().build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + final BidResponse enriched = result.bidResponse(); + assertThat(enriched.getExt()).isNotNull(); + assertThat(enriched.getExt().getPrebid()).isNotNull(); + final JsonNode passthrough = enriched.getExt().getPrebid().getPassthrough(); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo("signature"); + } + + @Test + public void shouldMergeId5SignatureWithExistingPassthrough() { + // given + final ObjectNode existingPassthrough = ObjectMapperProvider.mapper().createObjectNode(); + existingPassthrough.set("other", ObjectMapperProvider.mapper().createObjectNode() + .set("value", TextNode.valueOf("otherValue"))); + existingPassthrough.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("existing", TextNode.valueOf("preserved"))); + + final ExtBidResponse ext = ExtBidResponse.builder() + .prebid(ExtBidResponsePrebid.builder() + .passthrough(existingPassthrough) + .build()) + .build(); + final BidResponse bidResponse = BidResponse.builder().ext(ext).build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + final JsonNode passthrough = result.bidResponse().getExt().getPrebid().getPassthrough(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo("signature"); + assertThat(passthrough.get("optable").get("existing").asText()).isEqualTo("preserved"); + assertThat(passthrough.get("other").get("value").asText()).isEqualTo("otherValue"); + } + + @Test + public void shouldPreserveExistingPrebidFieldsWhenAddingPassthrough() { + // given + final ExtBidResponse ext = ExtBidResponse.builder() + .prebid(ExtBidResponsePrebid.builder().build()) + .build(); + final BidResponse bidResponse = BidResponse.builder().ext(ext).build(); + final AuctionResponsePayload payload = AuctionResponsePayloadImpl.of(bidResponse); + final Id5SignatureBidResponseEnricher enricher = + Id5SignatureBidResponseEnricher.of("signature", ObjectMapperProvider.mapper(), jsonMerger); + + // when + final AuctionResponsePayload result = enricher.apply(payload); + + // then + final ExtBidResponsePrebid prebid = result.bidResponse().getExt().getPrebid(); + assertThat(prebid).isNotNull(); + assertThat(prebid.getPassthrough().get("optable").get("id5_signature").asText()) + .isEqualTo("signature"); + } +} diff --git a/sample/configs/prebid-config-with-optable.yaml b/sample/configs/prebid-config-with-optable.yaml index 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} From 073b1d543b73a6c1ba3a04d78cc0b80e242f003b Mon Sep 17 00:00:00 2001 From: softcoder Date: Sun, 26 Jul 2026 16:40:37 +0200 Subject: [PATCH 05/15] optable-targeting: code cleanup --- .../optable/targeting/v1/core/Id5Resolver.java | 15 ++++++++++++--- .../v1/core/OptableAttributesResolver.java | 4 +++- .../optable/targeting/v1/core/QueryBuilder.java | 6 ++++-- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index d7a17c1365a..e9ba337fb3b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -14,6 +14,12 @@ public class Id5Resolver { + public static final String STR_OPTABLE_CO = "optable.co"; + public static final String STR_ID_5_SYNC_COM = "id5-sync.com"; + public static final String STR_OPTABLE = "optable"; + public static final String STR_REF = "ref"; + public static final String STR_SIGNATURE = "signature"; + private Id5Resolver() { } @@ -29,11 +35,14 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .map(User::getEids) .orElseGet(List::of) .stream() - .filter(it -> "optable.co".equals(it.getInserter()) && "id5-sync.com".equals(it.getSource())) + .filter(it -> STR_OPTABLE_CO.equals(it.getInserter()) && STR_ID_5_SYNC_COM.equals(it.getSource())) .map(Eid::getUids) .flatMap(Collection::stream) .map(Uid::getExt) - .map(it -> it.at("/optable/ref")) + .filter(Objects::nonNull) + .map(it -> it.get(STR_OPTABLE)) + .filter(Objects::nonNull) + .map(it -> it.get(STR_REF)) .filter(Objects::nonNull) .map(JsonNode::asText) .findFirst() @@ -45,7 +54,7 @@ public static String resolveId5Signature(TargetingResult targetingResult) { return Optional.ofNullable(targetingResult.getRefs()) .map(refs -> refs.get(ref)) - .map(it -> it.get("signature")) + .map(it -> it.get(STR_SIGNATURE)) .map(JsonNode::asText) .orElse(null); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index 0122fca451d..b655f04cb61 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -68,7 +68,9 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .map(ext -> ext.getProperty("optable")) .map(OptableAttributesResolver::parseExtUserOptable); - extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); + if (extUserOptable.isPresent()) { + extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); + } return builder.build(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 00aca36e8f8..9e6c0da251a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -49,7 +49,8 @@ private static String buildHidString(List ids, String hidPrefixesString) { if (CollectionUtils.isEmpty(ids) || StringUtils.isEmpty(hidPrefixesString)) { return StringUtils.EMPTY; } - final Map prefixToIdValue = ids.stream().collect(Collectors.toMap(Id::getName, it -> it)); + final Map prefixToIdValue = ids.stream() + .collect(Collectors.toMap(Id::getName, it -> it, (a, b) -> b)); if (MapUtils.isEmpty(prefixToIdValue)) { return StringUtils.EMPTY; } @@ -135,7 +136,8 @@ private static String buildAttributesString(OptableAttributes optableAttributes) }); Optional.ofNullable(optableAttributes.getId5Signature()) - .ifPresent(id5Signature -> sb.append("&id5_signature=").append(id5Signature)); + .ifPresent(id5Signature -> sb.append("&id5_signature=") + .append(URLEncoder.encode(id5Signature, StandardCharsets.UTF_8))); return sb.toString(); } From 65de1b63f71c45a6616ce536f8507ffea5a08ed3 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 19:37:18 +0200 Subject: [PATCH 06/15] optable-targeting: fix NPE when optableTargetingCall was never set at resolving id5Signature --- .../targeting/model/ModuleContext.java | 2 + .../v1/OptableBidderRequestHook.java | 2 + .../OptableTargetingAuctionResponseHook.java | 26 ++++------- ...eTargetingProcessedAuctionRequestHook.java | 2 + .../v1/OptableBidderRequestHookTest.java | 46 +++++++++++++++++++ ...tableTargetingAuctionResponseHookTest.java | 43 +++++++++-------- ...getingProcessedAuctionRequestHookTest.java | 44 ++++++++++++++++++ 7 files changed, 129 insertions(+), 36 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java index 5574a7a72d8..ac7e30eec8d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/ModuleContext.java @@ -35,6 +35,8 @@ public class ModuleContext { private boolean shouldSkipEnrichment; + private String id5Signature; + public static ModuleContext of(AuctionInvocationContext invocationContext) { final ModuleContext moduleContext = (ModuleContext) invocationContext.moduleContext(); return moduleContext != null ? moduleContext : new ModuleContext(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index 7c544a47126..a8926b0ded0 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()); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index a3f0467d7c9..1f98c18cc0d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -9,12 +9,10 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Status; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AnalyticTagsResolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.AuctionResponseValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidResponseEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; -import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5SignatureBidResponseEnricher; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -70,36 +68,32 @@ public Future> call(AuctionResponsePayl private Future> enrichedPayload(ModuleContext moduleContext) { final List targeting = moduleContext.getTargeting(); + final String id5Signature = moduleContext.getId5Signature(); - return moduleContext.getOptableTargetingCall() - .compose(targetingResult -> - CollectionUtils.isNotEmpty(targeting) - ? update(fullEnrichmentChain(moduleContext, targeting, targetingResult), moduleContext) - : update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) - .recover(throwable -> success(moduleContext)); + return CollectionUtils.isNotEmpty(targeting) + ? update(fullEnrichmentChain(targeting, id5Signature), moduleContext) + : update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } private Future> enrichedById5SignaturePayload( ModuleContext moduleContext) { + final String id5Signature = moduleContext.getId5Signature(); + return moduleContext.getOptableTargetingCall() .compose(targetingResult -> - update(id5SignatureEnrichmentChain(targetingResult), moduleContext)) + update(id5SignatureEnrichmentChain(id5Signature), moduleContext)) .recover(throwable -> success(moduleContext)); } - private PayloadUpdate fullEnrichmentChain(ModuleContext moduleContext, - final List targeting, - TargetingResult targetingResult) { - final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); + private PayloadUpdate fullEnrichmentChain(final List targeting, + final String id5Signature) { return BidResponseEnricher.of(targeting, objectMapper, jsonMerger) .andThen(Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger))::apply; } - private PayloadUpdate id5SignatureEnrichmentChain(TargetingResult targetingResult) { - final String id5Signature = Id5Resolver.resolveId5Signature(targetingResult); - + private PayloadUpdate id5SignatureEnrichmentChain(String id5Signature) { return Id5SignatureBidResponseEnricher.of(id5Signature, objectMapper, jsonMerger); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java index 384e5c21f1b..fc22fdc354c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHook.java @@ -11,6 +11,7 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.core.BidRequestEnricher; import org.prebid.server.hooks.modules.optable.targeting.v1.core.CompositeHookExecutionPlan; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; +import org.prebid.server.hooks.modules.optable.targeting.v1.core.Id5Resolver; import org.prebid.server.hooks.modules.optable.targeting.v1.core.PropertiesValidator; import org.prebid.server.hooks.modules.optable.targeting.v1.core.TargetingRequestExecutor; import org.prebid.server.hooks.v1.InvocationAction; @@ -105,6 +106,7 @@ private Future> enrichPayload( OptableTargetingProperties properties) { moduleContext.setTargeting(targetingResult.getAudience()); + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); final PayloadUpdate payloadUpdate = diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java index 2bcce42ac94..4180f8645cc 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -1,7 +1,9 @@ package org.prebid.server.hooks.modules.optable.targeting.v1; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.BidRequest; +import com.iab.openrtb.request.Eid; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -14,6 +16,7 @@ import org.prebid.server.hooks.modules.optable.targeting.model.EnrichmentStatus; import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.Status; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -23,6 +26,7 @@ import org.prebid.server.hooks.v1.bidder.BidderRequestPayload; import java.util.Collections; +import java.util.List; import java.util.Set; import java.util.concurrent.TimeoutException; @@ -170,6 +174,48 @@ public void shouldUpdateModuleContextWithTargetingOnSuccess() { .isEqualTo("success"); } + @Test + public void shouldSetId5SignatureOnModuleContextWhenTargetingResultHasId5Signature() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100); + moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); + + // when + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + } + + @Test + public void shouldNotSetId5SignatureOnModuleContextWhenTargetingResultHasNoId5Signature() { + // given + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("bidder1")); + moduleContext.setCallTargetingAPITimestamp(System.currentTimeMillis() - 100); + moduleContext.setOptableTargetingCall(Future.succeededFuture(givenTargetingResult())); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + when(invocationContext.bidder()).thenReturn("bidder1"); + + // when + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(moduleContext.getId5Signature()).isNull(); + } + @Test public void shouldReturnNoActionWithNoDataOutcomeWhenTargetingResultHasNoUser() { // given diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 29429910f8d..5b37eeec80f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -2,7 +2,6 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.iab.openrtb.request.Eid; import com.iab.openrtb.response.BidResponse; import io.vertx.core.Future; import org.junit.jupiter.api.BeforeEach; @@ -14,7 +13,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.ModuleContext; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Audience; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.AudienceId; -import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.core.ConfigResolver; import org.prebid.server.hooks.v1.InvocationAction; import org.prebid.server.hooks.v1.InvocationResult; @@ -134,19 +132,15 @@ public void shouldReturnResultWithUpdateActionWhenAdvertiserTargetingOptionIsOn( @Test public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { // given - final String refValue = "refValue"; final String signature = "id5Signature"; - final Eid id5Eid = givenId5Eid(refValue); - final ObjectNode refs = givenRefsObject(refValue, signature); - final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); - - when(invocationContext.moduleContext()).thenReturn(givenModuleContext( - List.of(new Audience( + final ModuleContext moduleContext = givenModuleContext(List.of( + new Audience( "provider", List.of(new AudienceId("audienceId")), "keyspace", - 1)), - Future.succeededFuture(targetingResult))); + 1))); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -181,14 +175,11 @@ public void shouldEnrichBidResponseWithBothTargetingKeywordsAndId5Signature() { @Test public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() { // given - final String refValue = "refValue"; final String signature = "id5Signature"; - final Eid id5Eid = givenId5Eid(refValue); - final ObjectNode refs = givenRefsObject(refValue, signature); - final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); - - when(invocationContext.moduleContext()).thenReturn( - givenModuleContext(null, Future.succeededFuture(targetingResult))); + final ModuleContext moduleContext = + givenModuleContext(null, Future.succeededFuture(givenEmptyTargetingResult())); + moduleContext.setId5Signature(signature); + when(invocationContext.moduleContext()).thenReturn(moduleContext); when(auctionResponsePayload.bidResponse()).thenReturn(givenBidResponse()); // when @@ -212,7 +203,7 @@ public void shouldEnrichBidResponseWithId5SignatureOnlyWhenNoTargeting() { } @Test - public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsPresent() { + public void shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargetingCallFails() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext( List.of(new Audience( @@ -227,13 +218,25 @@ public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsPrese final Future> future = target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final ObjectNode targeting = (ObjectNode) bidResponse.getSeatbid() + .getFirst() + .getBid() + .getFirst() + .getExt() + .get("prebid") + .get("targeting"); // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); + assertThat(targeting.get("keyspace").asText()).isEqualTo("audienceId"); } @Test diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 7387e8f85d5..0e149e857b2 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java @@ -156,6 +156,50 @@ void callShouldReturnResultWithUpdateActionWhenOptableTargetingReturnsTargeting( .containsExactly("id"); } + @Test + void callShouldSetId5SignatureOnModuleContextWhenTargetingResultContainsId5Signature() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult(List.of(id5Eid), null, refs))); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat((ModuleContext) future.result().moduleContext()) + .isNotNull() + .extracting(ModuleContext::getId5Signature) + .isEqualTo(signature); + } + + @Test + void callShouldLeaveId5SignatureNullWhenTargetingResultHasNoId5Signature() { + // given + when(auctionRequestPayload.bidRequest()).thenReturn(givenBidRequest()); + when(optableTargeting.getTargeting(any(), any(), any(), any())) + .thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + final Future> future = target.call(auctionRequestPayload, + invocationContext); + + // then + assertThat(future).isNotNull(); + assertThat(future.succeeded()).isTrue(); + assertThat((ModuleContext) future.result().moduleContext()) + .isNotNull() + .extracting(ModuleContext::getId5Signature) + .isNull(); + } + @Test void callShouldReturnResultWithUpdateActionWhenEarlyOptableCallIsEnabled() { // given From bceaf21b465a8de7d536e64847011baa3e4c4ca9 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:04:55 +0200 Subject: [PATCH 07/15] optable-targeting: Encode Targeting API query params --- .../targeting/v1/core/QueryBuilder.java | 7 +++-- .../targeting/v1/core/QueryBuilderTest.java | 30 +++++++++++++++++-- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 9e6c0da251a..ed3c5905222 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -58,7 +58,8 @@ private static String buildHidString(List ids, String hidPrefixesString) { final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) .map(prefixToIdValue::get) .filter(Objects::nonNull) - .map(it -> String.format("hid=%s:%s", it.getName(), it.getValue())) + .map(it -> String.format( + "hid=%s:%s", it.getName(), URLEncoder.encode(it.getValue(), StandardCharsets.UTF_8))) .collect(Collectors.joining("&")); if (StringUtils.isEmpty(hidParameters)) { return StringUtils.EMPTY; @@ -126,11 +127,11 @@ private static String buildAttributesString(OptableAttributes optableAttributes) .ifPresent(app -> { final String bundle = app.getBundle(); if (StringUtils.isNotEmpty(bundle)) { - sb.append("&bundle=").append(bundle); + sb.append("&bundle=").append(URLEncoder.encode(bundle, StandardCharsets.UTF_8)); final String ver = app.getVer(); if (StringUtils.isNotEmpty(ver)) { - sb.append("&ver=").append(ver); + sb.append("&ver=").append(URLEncoder.encode(ver, StandardCharsets.UTF_8)); } } }); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 64aff679be4..08f6ce82815 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -207,7 +207,7 @@ public void shouldBuildHidWhenHidPrefixesMatchIds() { final Query query = QueryBuilder.build(ids, optableAttributes, props); // then - assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0:0:0:0:0:0:0:1"); + assertThat(query.getHid()).isEqualTo("&hid=c:234&hid=i6:0%3A0%3A0%3A0%3A0%3A0%3A0%3A1"); } @Test @@ -223,7 +223,33 @@ public void shouldExcludeDeviceIpV6FromIdsString() { // then assertThat(query.getIds()).doesNotContain(Id.DEVICE_IP_V_6); - assertThat(query.getHid()).isEqualTo("&hid=i6:0:0:0:0:0:0:0:1"); + assertThat(query.getHid()).isEqualTo("&hid=i6:0%3A0%3A0%3A0%3A0%3A0%3A0%3A1"); + } + + @Test + public void shouldUrlEncodeHidValueWithSpecialCharacters() { + // given + final OptableTargetingProperties props = givenProperties(null, "c"); + final List ids = List.of(Id.of("c", "a b&c=d+e/f")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo("&hid=c:a+b%26c%3Dd%2Be%2Ff"); + } + + @Test + public void shouldLeaveHidValueUnchangedForAlphanumericValue() { + // given + final OptableTargetingProperties props = givenProperties(null, "c"); + final List ids = List.of(Id.of("c", "abc123")); + + // when + final Query query = QueryBuilder.build(ids, optableAttributes, props); + + // then + assertThat(query.getHid()).isEqualTo("&hid=c:abc123"); } @Test From 41429ca7c959ee61c216c4c6cf61d8b7721117fa Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:16:40 +0200 Subject: [PATCH 08/15] optable-targeting: improve hid prefixes parsing --- .../hooks/modules/optable/targeting/v1/core/QueryBuilder.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index ed3c5905222..0385ff294f2 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -56,6 +56,8 @@ private static String buildHidString(List ids, String hidPrefixesString) { } final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) + .map(String::trim) + .filter(StringUtils::isNotEmpty) .map(prefixToIdValue::get) .filter(Objects::nonNull) .map(it -> String.format( From c59b63f616267cc28ffbfca1d8d4b25770ab6f20 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:17:30 +0200 Subject: [PATCH 09/15] optable-targeting: Remove dead code --- .../hooks/modules/optable/targeting/v1/core/QueryBuilder.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 0385ff294f2..66391ee4634 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -51,9 +51,6 @@ private static String buildHidString(List ids, String hidPrefixesString) { } final Map prefixToIdValue = ids.stream() .collect(Collectors.toMap(Id::getName, it -> it, (a, b) -> b)); - if (MapUtils.isEmpty(prefixToIdValue)) { - return StringUtils.EMPTY; - } final String hidParameters = Arrays.stream(hidPrefixesString.split(",")) .map(String::trim) From a54cecd4da5097cf72c442790f0e26589e22c84c Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 20:48:09 +0200 Subject: [PATCH 10/15] optable-targeting: Improve id5 signature extraction from Targeting response --- .../targeting/v1/core/Id5Resolver.java | 25 ++++-- .../targeting/v1/core/QueryBuilder.java | 1 - .../targeting/v1/core/Id5ResolverTest.java | 90 +++++++++++++++++++ 3 files changed, 109 insertions(+), 7 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index e9ba337fb3b..908520b5f6f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -1,8 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Uid; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; @@ -29,7 +32,7 @@ public static String resolveId5Signature(TargetingResult targetingResult) { return null; } - final String ref = Optional.of(targetingResult) + final List refs = Optional.of(targetingResult) .map(TargetingResult::getOrtb2) .map(Ortb2::getUser) .map(User::getEids) @@ -37,6 +40,7 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .stream() .filter(it -> STR_OPTABLE_CO.equals(it.getInserter()) && STR_ID_5_SYNC_COM.equals(it.getSource())) .map(Eid::getUids) + .filter(Objects::nonNull) .flatMap(Collection::stream) .map(Uid::getExt) .filter(Objects::nonNull) @@ -45,17 +49,26 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .map(it -> it.get(STR_REF)) .filter(Objects::nonNull) .map(JsonNode::asText) - .findFirst() - .orElse(null); + .toList(); - if (ref == null) { + if (CollectionUtils.isEmpty(refs)) { return null; } - return Optional.ofNullable(targetingResult.getRefs()) - .map(refs -> refs.get(ref)) + final ObjectNode references = targetingResult.getRefs(); + if (references == null) { + return null; + } + + return refs.stream() + .map(references::get) + .filter(Objects::nonNull) .map(it -> it.get(STR_SIGNATURE)) + .filter(Objects::nonNull) + .filter(JsonNode::isValueNode) .map(JsonNode::asText) + .filter(StringUtils::isNotBlank) + .findFirst() .orElse(null); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 66391ee4634..ad3ba1efefd 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -1,7 +1,6 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; import org.apache.commons.collections4.CollectionUtils; -import org.apache.commons.collections4.MapUtils; import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java index b0b9be37b21..c38a0821d49 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -188,6 +188,96 @@ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { assertThat(result).isEqualTo("firstSignature"); } + @Test + public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { + // given + final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); + firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("firstRef"))); + + final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); + secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("secondRef"))); + + final Eid firstEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id1").ext(firstUidExt).build())) + .build(); + final Eid secondEid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf("secondSignature"))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(firstEid, secondEid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isEqualTo("secondSignature"); + } + + @Test + public void shouldReturnNullWhenRefEntrySignatureIsBlank() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", TextNode.valueOf(" "))); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { + // given + final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); + uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + .set("ref", TextNode.valueOf("refValue"))); + + final Eid eid = Eid.builder() + .source("id5-sync.com") + .inserter("optable.co") + .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) + .build(); + final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); + refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + .set("signature", ObjectMapperProvider.mapper().createObjectNode())); + final TargetingResult targetingResult = new TargetingResult( + List.of(), + new Ortb2(new User(List.of(eid), null)), + refs); + + // when + final String result = Id5Resolver.resolveId5Signature(targetingResult); + + // then + assertThat(result).isNull(); + } + @Test public void shouldReturnNullWhenRefEntryHasNoSignature() { // given From ea21f42cd16bcab54e0b2b6a0fd89c6ae29709c5 Mon Sep 17 00:00:00 2001 From: softcoder Date: Mon, 27 Jul 2026 22:08:49 +0200 Subject: [PATCH 11/15] optable-targeting: Code cleanup --- .../config/OptableTargetingConfig.java | 7 +- .../v1/core/ExtUserOptableResolver.java | 26 ++++++++ .../targeting/v1/core/Id5Resolver.java | 1 - .../optable/targeting/v1/core/IdsMapper.java | 18 +----- .../v1/core/OptableAttributesResolver.java | 26 +++----- .../v1/core/TargetingRequestExecutor.java | 10 ++- .../v1/OptableRawAuctionRequestHookTest.java | 3 +- ...getingProcessedAuctionRequestHookTest.java | 3 +- .../v1/core/ExtUserOptableResolverTest.java | 64 +++++++++++++++++++ .../core/OptableAttributesResolverTest.java | 16 ++--- .../configs/sample-app-settings-optable.yaml | 2 +- 11 files changed, 123 insertions(+), 53 deletions(-) create mode 100644 extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java create mode 100644 extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java index cc05cfe2a02..91ae6691bbf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/config/OptableTargetingConfig.java @@ -98,10 +98,11 @@ ConfigResolver configResolver(JsonMerger jsonMerger, OptableTargetingProperties @Bean TargetingRequestExecutor targetingRequestExecutor(OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - TimeoutFactory timeoutFactory) { + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory, + @Value("${logging.sampling-rate:0.01}") double logSamplingRate) { - return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); + return new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory, logSamplingRate); } @Bean diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java new file mode 100644 index 00000000000..1d7e081148e --- /dev/null +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolver.java @@ -0,0 +1,26 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; +import org.prebid.server.log.ConditionalLogger; +import org.prebid.server.log.LoggerFactory; + +public class ExtUserOptableResolver { + + private static final ConditionalLogger conditionalLogger = + new ConditionalLogger(LoggerFactory.getLogger(ExtUserOptableResolver.class)); + + private ExtUserOptableResolver() { + } + + public static ExtUserOptable resolveExtUserOptable(JsonNode node, double logSamplingRate) { + try { + return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class); + } catch (JsonProcessingException e) { + conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate); + return null; + } + } +} diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index 908520b5f6f..564d2c4a142 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -24,7 +24,6 @@ public class Id5Resolver { public static final String STR_SIGNATURE = "signature"; private Id5Resolver() { - } public static String resolveId5Signature(TargetingResult targetingResult) { diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java index a9b4c9c7311..ddb47141248 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/IdsMapper.java @@ -1,7 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; @@ -14,8 +12,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.Id; import org.prebid.server.hooks.modules.optable.targeting.model.OS; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; -import org.prebid.server.log.ConditionalLogger; -import org.prebid.server.log.LoggerFactory; import java.util.Collections; import java.util.HashMap; @@ -26,9 +22,6 @@ public class IdsMapper { - private static final ConditionalLogger conditionalLogger = - new ConditionalLogger(LoggerFactory.getLogger(IdsMapper.class)); - private static final Map STATIC_PPID_MAPPING = Map.of( "id5-sync.com", Id.ID5, "utiq.com", Id.UTIQ, @@ -60,7 +53,7 @@ private void addOptableIds(Map ids, User user) { final Optional extUserOptable = Optional.ofNullable(user) .map(User::getExt) .map(ext -> ext.getProperty("optable")) - .map(this::parseExtUserOptable); + .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate)); extUserOptable.map(ExtUserOptable::getEmail).ifPresent(it -> ids.put(Id.EMAIL, it)); extUserOptable.map(ExtUserOptable::getPhone).ifPresent(it -> ids.put(Id.PHONE, it)); @@ -68,15 +61,6 @@ private void addOptableIds(Map ids, User user) { extUserOptable.map(ExtUserOptable::getVid).ifPresent(it -> ids.put(Id.OPTABLE_VID, it)); } - private ExtUserOptable parseExtUserOptable(JsonNode node) { - try { - return objectMapper.treeToValue(node, ExtUserOptable.class); - } catch (JsonProcessingException e) { - conditionalLogger.warn("Can't parse $.ext.user.Optable tag", logSamplingRate); - return null; - } - } - private static void addDeviceIds(Map ids, Device device) { final String ifa = device != null ? device.getIfa() : null; final String os = device != null ? StringUtils.toRootLowerCase(device.getOs()) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java index b655f04cb61..39783e9bf34 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolver.java @@ -1,7 +1,5 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.JsonNode; import com.iab.openrtb.request.BidRequest; import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Regs; @@ -13,7 +11,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.App; import org.prebid.server.hooks.modules.optable.targeting.model.OptableAttributes; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; -import org.prebid.server.json.ObjectMapperProvider; import org.prebid.server.proto.openrtb.ext.request.ExtRegs; import org.prebid.server.proto.openrtb.ext.request.ExtUser; @@ -26,7 +23,10 @@ public class OptableAttributesResolver { private OptableAttributesResolver() { } - public static OptableAttributes resolveAttributes(AuctionContext auctionContext, Long timeout) { + public static OptableAttributes resolveAttributes(AuctionContext auctionContext, + Long timeout, + double logSamplingRate) { + final GppContext.Scope gppScope = auctionContext.getGppContext().scope(); final BidRequest bidRequest = auctionContext.getBidRequest(); @@ -63,26 +63,16 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, .gppSid(SetUtils.emptyIfNull(gppScope.getSectionsIds())); } - final Optional extUserOptable = Optional.ofNullable(bidRequest.getUser()) + Optional.ofNullable(bidRequest.getUser()) .map(User::getExt) .map(ext -> ext.getProperty("optable")) - .map(OptableAttributesResolver::parseExtUserOptable); - - if (extUserOptable.isPresent()) { - extUserOptable.map(ExtUserOptable::getId5Signature).ifPresent(builder::id5Signature); - } + .map(it -> ExtUserOptableResolver.resolveExtUserOptable(it, logSamplingRate)) + .map(ExtUserOptable::getId5Signature) + .ifPresent(builder::id5Signature); return builder.build(); } - private static ExtUserOptable parseExtUserOptable(JsonNode node) { - try { - return ObjectMapperProvider.mapper().treeToValue(node, ExtUserOptable.class); - } catch (JsonProcessingException e) { - return null; - } - } - private static App resolveApp(AuctionContext auctionContext) { final com.iab.openrtb.request.App app = auctionContext.getBidRequest().getApp(); return app != null ? App.of(app.getBundle(), app.getVer()) : null; diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 36cd33c7724..709b18979f5 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java @@ -29,14 +29,17 @@ public class TargetingRequestExecutor { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; private final TimeoutFactory timeoutFactory; + private double logSamplingRate; public TargetingRequestExecutor(OptableTargeting optableTargeting, - UserFpdActivityMask userFpdActivityMask, - TimeoutFactory timeoutFactory) { + UserFpdActivityMask userFpdActivityMask, + TimeoutFactory timeoutFactory, + double logSamplingRate) { this.optableTargeting = Objects.requireNonNull(optableTargeting); this.userFpdActivityMask = Objects.requireNonNull(userFpdActivityMask); this.timeoutFactory = ObjectUtils.requireNonEmpty(timeoutFactory); + this.logSamplingRate = logSamplingRate; } public Future makeRequest(AuctionRequestPayload payload, @@ -51,7 +54,8 @@ public Future makeRequest(AuctionRequestPayload payload, : timeoutFactory.create(getHookTimeout(invocationContext).remaining() + apiTimeout); final OptableAttributes attributes = OptableAttributesResolver.resolveAttributes( invocationContext.auctionContext(), - properties.getTimeout()); + properties.getTimeout(), + logSamplingRate); return optableTargeting.getTargeting(properties, bidRequest, attributes, timeout); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java index 059db74ce0e..4e1a5b3e5f1 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableRawAuctionRequestHookTest.java @@ -63,7 +63,8 @@ public void setUp() { when(userFpdActivityMask.maskDevice(any(), anyBoolean(), anyBoolean())) .thenAnswer(answer -> answer.getArgument(0)); configResolver = new ConfigResolver(mapper, jsonMerger, givenOptableTargetingProperties(false)); - targetingRequestExecutor = new TargetingRequestExecutor(optableTargeting, userFpdActivityMask, timeoutFactory); + targetingRequestExecutor = new TargetingRequestExecutor( + optableTargeting, userFpdActivityMask, timeoutFactory, 0.01); target = new OptableRawAuctionRequestHook( configResolver, targetingRequestExecutor, bidderEnrichmentSampler, CompositeHookExecutionPlan.of(ExecutionPlan.empty()), 0.01); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingProcessedAuctionRequestHookTest.java index 0e149e857b2..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); diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java new file mode 100644 index 00000000000..b3142c173f2 --- /dev/null +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/ExtUserOptableResolverTest.java @@ -0,0 +1,64 @@ +package org.prebid.server.hooks.modules.optable.targeting.v1.core; + +import com.fasterxml.jackson.databind.node.ObjectNode; +import com.fasterxml.jackson.databind.node.TextNode; +import org.junit.jupiter.api.Test; +import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.ExtUserOptable; +import org.prebid.server.json.ObjectMapperProvider; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ExtUserOptableResolverTest { + + @Test + public void shouldResolveExtUserOptableWhenNodeIsValid() { + // given + final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode(); + node.set("email", TextNode.valueOf("user@example.com")); + node.set("phone", TextNode.valueOf("123")); + node.set("zip", TextNode.valueOf("321")); + node.set("vid", TextNode.valueOf("vid")); + node.set("id5_signature", TextNode.valueOf("signature")); + + // when + final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0); + + // then + assertThat(result).isNotNull() + .returns("user@example.com", ExtUserOptable::getEmail) + .returns("123", ExtUserOptable::getPhone) + .returns("321", ExtUserOptable::getZip) + .returns("vid", ExtUserOptable::getVid) + .returns("signature", ExtUserOptable::getId5Signature); + } + + @Test + public void shouldReturnNullWhenNodeIsNotParseable() { + // given + final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode(); + node.set("email", ObjectMapperProvider.mapper().createArrayNode().add(1).add(2)); + + // when + final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0); + + // then + assertThat(result).isNull(); + } + + @Test + public void shouldResolveExtUserOptableWhenNodeIsEmpty() { + // given + final ObjectNode node = ObjectMapperProvider.mapper().createObjectNode(); + + // when + final ExtUserOptable result = ExtUserOptableResolver.resolveExtUserOptable(node, 1.0); + + // then + assertThat(result).isNotNull() + .returns(null, ExtUserOptable::getEmail) + .returns(null, ExtUserOptable::getPhone) + .returns(null, ExtUserOptable::getZip) + .returns(null, ExtUserOptable::getVid) + .returns(null, ExtUserOptable::getId5Signature); + } +} diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java index 276c3f30c88..51adfda9952 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableAttributesResolverTest.java @@ -60,7 +60,7 @@ public void shouldResolveGdprAttributesForORTB26WhenConsentIsValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -79,7 +79,7 @@ public void shouldResolveGdprAttributesForORTB25WhenConsentIsValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -100,7 +100,7 @@ public void shouldNotResolveTcfAttributesWhenConsentIsNotValid() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -121,7 +121,7 @@ public void shouldResolveGppAttributes() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -144,7 +144,7 @@ public void shouldResolveAppWhenAppIsPresent() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -159,7 +159,7 @@ public void shouldNotResolveAppWhenAppIsAbsent() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -174,7 +174,7 @@ public void shouldResolveId5SignatureWhenPresentInUserExtOptable() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() @@ -189,7 +189,7 @@ public void shouldNotResolveId5SignatureWhenAbsentInUserExtOptable() { // when final OptableAttributes result = OptableAttributesResolver.resolveAttributes( - auctionContext, properties.getTimeout()); + auctionContext, properties.getTimeout(), 0.01); // then assertThat(result).isNotNull() diff --git a/sample/configs/sample-app-settings-optable.yaml b/sample/configs/sample-app-settings-optable.yaml index 7b4af19a7e2..b05448d65ca 100644 --- a/sample/configs/sample-app-settings-optable.yaml +++ b/sample/configs/sample-app-settings-optable.yaml @@ -31,7 +31,7 @@ accounts: enrich-app: true id-prefix-order: "e,v,c" hid-prefixes: "c,i6" - ppid-mapping: { "pubmatic.com": "c" } + ppid-mapping: { "pubcid.org": "c" } adserver-targeting: true cache: enabled: false From 903aebb2ad56725ae87dfa2085f806d3e5ebf4d0 Mon Sep 17 00:00:00 2001 From: softcoder Date: Tue, 28 Jul 2026 09:08:09 +0200 Subject: [PATCH 12/15] optable-targeting: update README.md --- extra/modules/optable-targeting/README.md | 51 ++++++++++++++++++++++- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 9e3b76c6446..901ad2fa40f 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -203,6 +203,7 @@ would result in this nesting in the JSON configuration: | adserver-targeting | no | boolean | false | If set to true - will add the Optable-specific adserver targeting keywords into the PBS response for every `seatbid[].bid[].ext.prebid.targeting` | | timeout | no | integer | none | A soft timeout (in ms) sent as a hint to the Targeting API endpoint to limit the request times to Optable's external tokenizer services | | id-prefix-order | no | string | none | An optional string of comma separated id prefixes that prioritizes and specifies the order in which ids are provided to Targeting API in a query string. F.e. "c,c1,id5" will guarantee that Targeting API will see id=c:...,c1:...,id5:... if these ids are provided. id-prefixes not mentioned in this list will be added in arbitrary order after the priority prefix ids. This affects Targeting API processing logic | +| hid-prefixes | no | string | none | An optional string of comma separated id prefixes that should additionally be sent to the Targeting API as resolver hints in `hid=prefix:value` query parameters. See the section on Resolver Hints (hid) below for more detail. | | enrichment-percentage | no | integer | 100 | Default percentage (0-100) of bid requests per bidder that will receive enrichment data. Set to 100 to enrich all requests, 0 to disable enrichment by default. | | bidder-enrichment-percentages | no | map | none | Per-bidder overrides for `enrichment-percentage`. Keys are bidder names, values are percentages (0-100). F.e. `{"appnexus": 75, "criteo": 0}` enriches 75% of appnexus requests and none for criteo. Bidders not listed in this map fall back to the default `enrichment-percentage` (100% unless overridden). | | enrich-web | no | boolean | true | Whether to enrich web traffic (requests with a `site` object). | @@ -239,8 +240,8 @@ on identifier types. Targeting API accepts multiple id parameters - and their or ### Optable input erasure -**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` fields will be removed by the module from the original -OpenRTB request before being sent to bidders. +**Note**: `user.ext.optable.email`, `.phone`, `.zip`, `.vid` and `.id5_signature` fields will be removed by the module +from the original OpenRTB request before being sent to bidders. ### Publisher Provided IDs (PPID) Mapping @@ -263,6 +264,52 @@ ppid-mapping: {"id5-sync.com": "c1"} This will lead to id5 ID supplied as `id=c1:...` to the Targeting API. +### Resolver Hints (`hid`) + +In addition to the regular `id=prefix:value` parameters, the module can forward selected identifiers to the Targeting +API as resolver hints using the `hid=prefix:value` query parameter form. The set of prefixes that should be sent as +`hid` is configured via the `hid-prefixes` parameter, which accepts a comma-separated list of prefix names, f.e.: + +```yaml +hid-prefixes: "c, i6" +``` + +## Targeting API Query Attributes + +In addition to the identifier parameters, the module forwards the following attributes as query string parameters to +the Targeting API: + +| Attribute | Source | +|------------------|--------------------------------------------------------------------------------------------------------------------------------------------| +| `gdpr` | `1` if GDPR applies to the request, `0` otherwise. | +| `gdpr_consent` | TCF consent string, sent when available and the consent is valid. | +| `gpp` | GPP string, sent when available in the resolved GPP context. | +| `gpp_sid` | Comma-separated list of active GPP section IDs (limited to the first two), sent when the set is non-empty. | +| `timeout` | Soft timeout hint (in ms, suffixed with `ms`), sent when the module-level `timeout` property is configured. | +| `osdk` | Always set to `prebid-server`, identifying the caller. | +| `bundle` | App bundle identifier, URL-encoded. Sent when the incoming request has an `app` object (`request.app.bundle`) and the bundle is non-empty. | +| `ver` | App version, URL-encoded. Sent only when `bundle` is non-empty and `request.app.ver` is present and non-empty. | +| `id5_signature` | ID5 signature, URL-encoded. Sent when the resolved `user.ext.optable.id5_signature` is present in the incoming request | + +### App bundle and version + +For app traffic (requests with an `app` object) the module forwards the application's bundle identifier as the +`bundle=` query parameter and, when available, its version as `ver=`. Both values are URL-encoded. The `ver` parameter +is only sent when `bundle` is present and non-empty — requests with a version but no bundle will not produce either +parameter. + +### ID5 signature + +The ID5 signature is propagated to the Targeting API in two ways: + +1. **Request-side signature** — when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value + is sent to the Targeting API as the `id5_signature=` query parameter. +2. **Response-side signature** — when the Targeting API response contains refs with an ID5 signature, the module + resolves the signature through the matching optable-eid and propagates it back into the request context, where it + is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use. + +The `id5_signature` field is also part of the Optable input erasure — see below. + ## Analytics Tags The following 2 analytics tags are written by the module: From 9f5bef2e95704a3014bbd29b6fd9db35c1f15ea6 Mon Sep 17 00:00:00 2001 From: softcoder Date: Tue, 28 Jul 2026 16:52:26 +0200 Subject: [PATCH 13/15] optable-targeting: Code cleanup --- extra/modules/optable-targeting/README.md | 2 +- .../v1/OptableBidderRequestHook.java | 23 +++++- .../OptableTargetingAuctionResponseHook.java | 14 +--- .../targeting/v1/core/Id5Resolver.java | 19 ++--- .../targeting/v1/core/QueryBuilder.java | 4 +- .../v1/core/TargetingRequestExecutor.java | 2 +- .../v1/OptableBidderRequestHookTest.java | 55 +++++++++++++ ...tableTargetingAuctionResponseHookTest.java | 25 ++++-- .../targeting/v1/core/Id5ResolverTest.java | 81 ++++++++++--------- 9 files changed, 155 insertions(+), 70 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 901ad2fa40f..1dae9829156 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -308,7 +308,7 @@ The ID5 signature is propagated to the Targeting API in two ways: resolves the signature through the matching optable-eid and propagates it back into the request context, where it is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use. -The `id5_signature` field is also part of the Optable input erasure — see below. +The `id5_signature` field is also part of the Optable input erasure — see above. ## Analytics Tags diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHook.java index a8926b0ded0..6128343a48e 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 @@ -84,7 +84,7 @@ private Future> failedAction(ModuleContex final Tags analyticsTags = AnalyticTagsResolver.toBidderEnrichRequestAnalyticTags( bidder, outcome, executionTime); - return noAction(moduleContext, analyticsTags); + return noActionResponse(moduleContext, analyticsTags); } private static boolean hasEnrichmentData(TargetingResult targetingResult) { @@ -100,6 +100,27 @@ private static long calcExecutionTime(ModuleContext moduleContext) { } private Future> noAction(ModuleContext moduleContext, Tags analyticsTags) { + final Future targetingCall = moduleContext.getOptableTargetingCall(); + if (targetingCall != null) { + return targetingCall.compose(targetingResult -> { + moduleContext.setId5Signature(Id5Resolver.resolveId5Signature(targetingResult)); + + return Future.succeededFuture( + InvocationResultImpl.builder() + .status(InvocationStatus.success) + .action(InvocationAction.no_action) + .analyticsTags(analyticsTags) + .moduleContext(moduleContext) + .build()); + }); + } + + return noActionResponse(moduleContext, analyticsTags); + } + + private Future> noActionResponse(ModuleContext moduleContext, + Tags analyticsTags) { + return Future.succeededFuture( InvocationResultImpl.builder() .status(InvocationStatus.success) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java index 1f98c18cc0d..8b0e233a003 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHook.java @@ -80,10 +80,7 @@ private Future> enrichedById5SignatureP final String id5Signature = moduleContext.getId5Signature(); - return moduleContext.getOptableTargetingCall() - .compose(targetingResult -> - update(id5SignatureEnrichmentChain(id5Signature), moduleContext)) - .recover(throwable -> success(moduleContext)); + return update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } private PayloadUpdate fullEnrichmentChain(final List targeting, @@ -112,13 +109,8 @@ private Future> update( } private Future> success(ModuleContext moduleContext) { - return Future.succeededFuture( - InvocationResultImpl.builder() - .status(InvocationStatus.success) - .action(InvocationAction.no_action) - .moduleContext(moduleContext) - .analyticsTags(AnalyticTagsResolver.toEnrichResponseAnalyticTags(moduleContext)) - .build()); + final String id5Signature = moduleContext.getId5Signature(); + return update(id5SignatureEnrichmentChain(id5Signature), moduleContext); } @Override diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java index 564d2c4a142..4349ad21240 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5Resolver.java @@ -17,11 +17,11 @@ public class Id5Resolver { - public static final String STR_OPTABLE_CO = "optable.co"; - public static final String STR_ID_5_SYNC_COM = "id5-sync.com"; - public static final String STR_OPTABLE = "optable"; - public static final String STR_REF = "ref"; - public static final String STR_SIGNATURE = "signature"; + public static final String OPTABLE_INSERTER = "optable.co"; + public static final String ID5_SOURCE = "id5-sync.com"; + public static final String OPTABLE = "optable"; + public static final String REF = "ref"; + public static final String SIGNATURE = "signature"; private Id5Resolver() { } @@ -37,15 +37,15 @@ public static String resolveId5Signature(TargetingResult targetingResult) { .map(User::getEids) .orElseGet(List::of) .stream() - .filter(it -> STR_OPTABLE_CO.equals(it.getInserter()) && STR_ID_5_SYNC_COM.equals(it.getSource())) + .filter(it -> OPTABLE_INSERTER.equals(it.getInserter()) && ID5_SOURCE.equals(it.getSource())) .map(Eid::getUids) .filter(Objects::nonNull) .flatMap(Collection::stream) .map(Uid::getExt) .filter(Objects::nonNull) - .map(it -> it.get(STR_OPTABLE)) + .map(it -> it.get(OPTABLE)) .filter(Objects::nonNull) - .map(it -> it.get(STR_REF)) + .map(it -> it.get(REF)) .filter(Objects::nonNull) .map(JsonNode::asText) .toList(); @@ -62,9 +62,10 @@ public static String resolveId5Signature(TargetingResult targetingResult) { return refs.stream() .map(references::get) .filter(Objects::nonNull) - .map(it -> it.get(STR_SIGNATURE)) + .map(it -> it.get(SIGNATURE)) .filter(Objects::nonNull) .filter(JsonNode::isValueNode) + .filter(it -> !it.isNull()) .map(JsonNode::asText) .filter(StringUtils::isNotBlank) .findFirst() diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index ad3ba1efefd..0417855a6ff 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -79,9 +79,7 @@ private static String buildIdsString(List ids, String idPrefixOrder) { final StringBuilder sb = new StringBuilder(); for (Id id : reorderedIds) { sb.append("&id="); - sb.append(URLEncoder.encode( - "%s:%s".formatted(id.getName(), id.getValue()), - StandardCharsets.UTF_8)); + sb.append(URLEncoder.encode(String.format("%s:%s", id.getName(), id.getValue()), StandardCharsets.UTF_8)); } return sb.toString(); diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java index 709b18979f5..8a93df9fbdf 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/TargetingRequestExecutor.java @@ -29,7 +29,7 @@ public class TargetingRequestExecutor { private final OptableTargeting optableTargeting; private final UserFpdActivityMask userFpdActivityMask; private final TimeoutFactory timeoutFactory; - private double logSamplingRate; + private final double logSamplingRate; public TargetingRequestExecutor(OptableTargeting optableTargeting, UserFpdActivityMask userFpdActivityMask, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java index 4180f8645cc..9ae7f6e4d96 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableBidderRequestHookTest.java @@ -78,6 +78,61 @@ public void shouldReturnNoActionWhenPerBidderEnrichmentIsDisabled() { assertThat(result.moduleContext()).isSameAs(moduleContext); } + @Test + public void shouldSetId5SignatureOnModuleContextWhenPerBidderEnrichmentIsDisabledAndTargetingCallIsPresent() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenOptableTargetingProperties(false)); + moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + } + + @Test + public void shouldSetId5SignatureOnModuleContextWhenBidderNotInEnrichmentSetAndTargetingCallIsPresent() { + // given + final String refValue = "refValue"; + final String signature = "id5Signature"; + final Eid id5Eid = givenId5Eid(refValue); + final ObjectNode refs = givenRefsObject(refValue, signature); + final TargetingResult targetingResult = givenTargetingResult(List.of(id5Eid), null, refs); + + final ModuleContext moduleContext = givenModuleContextWithProperties( + givenPropertiesWithPerBidderEnrichmentEnabled()); + moduleContext.setBiddersToEnrich(Set.of("otherBidder")); + moduleContext.setOptableTargetingCall(Future.succeededFuture(targetingResult)); + when(invocationContext.moduleContext()).thenReturn(moduleContext); + + // when + final Future> future = + target.call(bidderRequestPayload, invocationContext); + + // then + assertThat(future.succeeded()).isTrue(); + final InvocationResult result = future.result(); + assertThat(result).isNotNull() + .returns(InvocationStatus.success, InvocationResult::status) + .returns(InvocationAction.no_action, InvocationResult::action); + assertThat(moduleContext.getId5Signature()).isEqualTo(signature); + } + @Test public void shouldReturnNoActionWhenBiddersToEnrichIsEmpty() { // given diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java index 5b37eeec80f..19632668f63 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/OptableTargetingAuctionResponseHookTest.java @@ -59,7 +59,7 @@ public void shouldHaveCode() { } @Test - public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { + public void shouldReturnResultWithUpdateActionAndWithPBSAnalyticsTags() { // given when(invocationContext.moduleContext()).thenReturn( givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); @@ -75,7 +75,7 @@ public void shouldReturnResultWithNoActionAndWithPBSAnalyticsTags() { final InvocationResult result = future.result(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); assertThat(result.analyticsTags()) .extracting(Tags::activities) .extracting(List::getFirst) @@ -240,7 +240,7 @@ public void shouldReturnUpdateActionWhenTargetingIsPresentEvenIfOptableTargeting } @Test - public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { + public void shouldReturnUpdateActionWhenOptableTargetingCallFailsAndTargetingIsEmpty() { // given when(invocationContext.moduleContext()).thenReturn( givenModuleContext(null, Future.failedFuture(new RuntimeException("error")))); @@ -255,11 +255,11 @@ public void shouldReturnNoActionWhenOptableTargetingCallFailsAndTargetingIsEmpty assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); } @Test - public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { + public void shouldReturnResultWithUpdateActionWhenTargetingCallFailsAndNoBids() { // given when(invocationContext.moduleContext()).thenReturn(givenModuleContext(List.of( new Audience( @@ -279,27 +279,36 @@ public void shouldReturnResultWithNoActionWhenAdvertiserTargetingOptionIsOff() { assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); } @Test - public void shouldReturnSuccessWhenSkipEnrichmentIsTrue() { + public void shouldReturnUpdateActionWithId5SignatureWhenSkipEnrichmentIsTrue() { // given + final String signature = "id5Signature"; final ModuleContext moduleContext = givenModuleContext(); moduleContext.setShouldSkipEnrichment(true); + moduleContext.setId5Signature(signature); when(invocationContext.moduleContext()).thenReturn(moduleContext); // when final Future> future = target.call(auctionResponsePayload, invocationContext); final InvocationResult result = future.result(); + final BidResponse bidResponse = result + .payloadUpdate() + .apply(AuctionResponsePayloadImpl.of(givenBidResponse())) + .bidResponse(); + final JsonNode passthrough = bidResponse.getExt().getPrebid().getPassthrough(); // then assertThat(future).isNotNull(); assertThat(future.succeeded()).isTrue(); assertThat(result).isNotNull() .returns(InvocationStatus.success, InvocationResult::status) - .returns(InvocationAction.no_action, InvocationResult::action); + .returns(InvocationAction.update, InvocationResult::action); + assertThat(passthrough).isNotNull(); + assertThat(passthrough.get("optable").get("id5_signature").asText()).isEqualTo(signature); } private ObjectNode givenAccountConfig(boolean cacheEnabled) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java index c38a0821d49..41dc2b83ff7 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/Id5ResolverTest.java @@ -1,9 +1,11 @@ package org.prebid.server.hooks.modules.optable.targeting.v1.core; +import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; import com.fasterxml.jackson.databind.node.TextNode; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Uid; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; @@ -16,6 +18,13 @@ public class Id5ResolverTest { + private ObjectMapper mapper; + + @BeforeEach + public void setUp() { + mapper = ObjectMapperProvider.mapper(); + } + @Test public void shouldReturnNullWhenTargetingResultIsNull() { // when @@ -75,8 +84,8 @@ public void shouldReturnNullWhenEidsDoNotMatchOptableAndId5Source() { @Test public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -99,8 +108,8 @@ public void shouldReturnNullWhenMatchingEidButRefsAreAbsent() { @Test public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -108,8 +117,8 @@ public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("otherRef", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("otherRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("signatureValue"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -126,8 +135,8 @@ public void shouldReturnNullWhenRefsDoNotContainResolvedRef() { @Test public void shouldReturnSignatureWhenAllConditionsAreMet() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -135,8 +144,8 @@ public void shouldReturnSignatureWhenAllConditionsAreMet() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() .set("signature", TextNode.valueOf("signatureValue"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -153,12 +162,12 @@ public void shouldReturnSignatureWhenAllConditionsAreMet() { @Test public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { // given - final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); - firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode firstUidExt = mapper.createObjectNode(); + firstUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("firstRef"))); - final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); - secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode secondUidExt = mapper.createObjectNode(); + secondUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("secondRef"))); final Eid firstEid = Eid.builder() @@ -171,10 +180,10 @@ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("firstRef", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("firstRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("firstSignature"))); - refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + refs.set("secondRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("secondSignature"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -191,12 +200,12 @@ public void shouldReturnSignatureWhenMultipleMatchingEidsExist() { @Test public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { // given - final ObjectNode firstUidExt = ObjectMapperProvider.mapper().createObjectNode(); - firstUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode firstUidExt = mapper.createObjectNode(); + firstUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("firstRef"))); - final ObjectNode secondUidExt = ObjectMapperProvider.mapper().createObjectNode(); - secondUidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode secondUidExt = mapper.createObjectNode(); + secondUidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("secondRef"))); final Eid firstEid = Eid.builder() @@ -209,8 +218,8 @@ public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id2").ext(secondUidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("secondRef", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("secondRef", mapper.createObjectNode() .set("signature", TextNode.valueOf("secondSignature"))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -227,8 +236,8 @@ public void shouldReturnSignatureFromSecondMatchingEidWhenFirstRefNotInRefs() { @Test public void shouldReturnNullWhenRefEntrySignatureIsBlank() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -236,8 +245,8 @@ public void shouldReturnNullWhenRefEntrySignatureIsBlank() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() .set("signature", TextNode.valueOf(" "))); final TargetingResult targetingResult = new TargetingResult( List.of(), @@ -254,8 +263,8 @@ public void shouldReturnNullWhenRefEntrySignatureIsBlank() { @Test public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -263,9 +272,9 @@ public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() - .set("signature", ObjectMapperProvider.mapper().createObjectNode())); + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() + .set("signature", mapper.createObjectNode())); final TargetingResult targetingResult = new TargetingResult( List.of(), new Ortb2(new User(List.of(eid), null)), @@ -281,8 +290,8 @@ public void shouldReturnNullWhenRefEntrySignatureIsContainerNode() { @Test public void shouldReturnNullWhenRefEntryHasNoSignature() { // given - final ObjectNode uidExt = ObjectMapperProvider.mapper().createObjectNode(); - uidExt.set("optable", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode uidExt = mapper.createObjectNode(); + uidExt.set("optable", mapper.createObjectNode() .set("ref", TextNode.valueOf("refValue"))); final Eid eid = Eid.builder() @@ -290,8 +299,8 @@ public void shouldReturnNullWhenRefEntryHasNoSignature() { .inserter("optable.co") .uids(List.of(Uid.builder().id("id").ext(uidExt).build())) .build(); - final ObjectNode refs = ObjectMapperProvider.mapper().createObjectNode(); - refs.set("refValue", ObjectMapperProvider.mapper().createObjectNode() + final ObjectNode refs = mapper.createObjectNode(); + refs.set("refValue", mapper.createObjectNode() .set("other", TextNode.valueOf("value"))); final TargetingResult targetingResult = new TargetingResult( List.of(), From 144d1e6919350a47918d7a9a5566d723fb926972 Mon Sep 17 00:00:00 2001 From: softcoder Date: Tue, 28 Jul 2026 20:30:32 +0200 Subject: [PATCH 14/15] optable-targeting: Update cache key --- .../optable/targeting/model/Query.java | 4 +- .../targeting/v1/core/QueryBuilder.java | 26 +++- .../targeting/v1/net/CachedAPIClient.java | 11 +- .../optable/targeting/v1/BaseOptableTest.java | 2 +- .../targeting/v1/core/QueryBuilderTest.java | 134 ++++++++++++++++++ .../targeting/v1/net/CachedAPIClientTest.java | 89 ++++++++++++ 6 files changed, 261 insertions(+), 5 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java index 19b725ab666..9b24ed2ed9d 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/Query.java @@ -11,7 +11,9 @@ public class Query { String attributes; + String hidAttributes; + public String toQueryString() { - return ids + hid + attributes; + return ids + hid + attributes + hidAttributes; } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java index 0417855a6ff..3278fb8de1a 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilder.java @@ -41,7 +41,8 @@ public static Query build(List ids, OptableAttributes optableAttributes, return Query.of( buildIdsString(ids, idPrefixOrder), buildHidString(ids, hidPrefixes), - buildAttributesString(optableAttributes)); + buildAttributesString(optableAttributes), + buildHidAttributesString(optableAttributes)); } private static String buildHidString(List ids, String hidPrefixesString) { @@ -138,4 +139,27 @@ private static String buildAttributesString(OptableAttributes optableAttributes) return sb.toString(); } + + private static String buildHidAttributesString(OptableAttributes optableAttributes) { + final StringBuilder sb = new StringBuilder(); + + Optional.ofNullable(optableAttributes.getApp()) + .ifPresent(app -> { + final String bundle = app.getBundle(); + if (StringUtils.isNotEmpty(bundle)) { + sb.append("&bundle=").append(URLEncoder.encode(bundle, StandardCharsets.UTF_8)); + + final String ver = app.getVer(); + if (StringUtils.isNotEmpty(ver)) { + sb.append("&ver=").append(URLEncoder.encode(ver, StandardCharsets.UTF_8)); + } + } + }); + + Optional.ofNullable(optableAttributes.getId5Signature()) + .ifPresent(id5Signature -> sb.append("&id5_signature=") + .append(URLEncoder.encode(id5Signature, StandardCharsets.UTF_8))); + + return sb.toString(); + } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java index eabccfdbff0..ada03ab1528 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClient.java @@ -53,12 +53,19 @@ public Future getTargeting(OptableTargetingProperties propertie } private String createCachingKey(String tenant, String origin, List ips, Query query, boolean encodeQuery) { - return "%s:%s:%s:%s".formatted( + return "%s:%s:%s:%s:%s:%s".formatted( tenant, origin, ips.getFirst(), encodeQuery ? URLEncoder.encode(query.getIds(), StandardCharsets.UTF_8) - : query.getIds()); + : query.getIds(), + encodeQuery && query.getHid() != null + ? URLEncoder.encode(query.getHid(), StandardCharsets.UTF_8) + : query.getHid(), + encodeQuery && query.getHidAttributes() != null + ? URLEncoder.encode(query.getHidAttributes(), StandardCharsets.UTF_8) + : query.getHidAttributes() + ); } } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java index 2ebf65c7b6e..77ccb5be730 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/BaseOptableTest.java @@ -301,7 +301,7 @@ protected OptableTargetingProperties givenOptableTargetingProperties(String key, } protected Query givenQuery() { - return Query.of("?que", "r", "y"); + return Query.of("?que", "r", "y", ""); } protected ObjectNode givenAccountConfig(String key, String tenant, String origin, boolean cacheEnabled) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java index 08f6ce82815..4b329d6c136 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/QueryBuilderTest.java @@ -336,6 +336,140 @@ public void shouldNotAppendBundleAndVerWhenAppIsNull() { assertThat(query).doesNotContain("&bundle=", "&ver="); } + @Test + public void shouldBuildHidAttributesWithBundleAndVerWhenAppIsPresent() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()) + .isEqualTo("&bundle=com.example.app&ver=1.2.3"); + } + + @Test + public void shouldBuildHidAttributesWithBundleOnlyWhenVerIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEqualTo("&bundle=com.example.app"); + } + + @Test + public void shouldNotBuildHidAttributesWithBundleWhenBundleIsEmpty() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("", "1.2.3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).doesNotContain("&bundle=", "&ver="); + } + + @Test + public void shouldNotBuildHidAttributesWithBundleWhenAppIsNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEmpty(); + } + + @Test + public void shouldBuildHidAttributesWithId5SignatureWhenPresent() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .id5Signature("signature") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEqualTo("&id5_signature=signature"); + } + + @Test + public void shouldNotBuildHidAttributesWithId5SignatureWhenNull() { + // given + final OptableAttributes attributes = OptableAttributes.builder().build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).doesNotContain("&id5_signature="); + } + + @Test + public void shouldIncludeHidAttributesInToQueryString() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example.app", "1.2.3")) + .id5Signature("signature") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final String queryString = QueryBuilder.build(ids, attributes, properties()).toQueryString(); + + // then + assertThat(queryString).endsWith("&bundle=com.example.app&ver=1.2.3&id5_signature=signature"); + } + + @Test + public void shouldAppendBundleAndVerToHidAttributesWithSpecialCharacters() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .app(App.of("com.example app", "1.2 3")) + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()) + .isEqualTo("&bundle=com.example+app&ver=1.2+3"); + } + + @Test + public void shouldAppendId5SignatureToHidAttributesWithSpecialCharacters() { + // given + final OptableAttributes attributes = OptableAttributes.builder() + .id5Signature("a b&c=d") + .build(); + final List ids = List.of(Id.of(Id.EMAIL, "email")); + + // when + final Query query = QueryBuilder.build(ids, attributes, properties()); + + // then + assertThat(query.getHidAttributes()).isEqualTo("&id5_signature=a+b%26c%3Dd"); + } + @Test public void shouldAppendId5SignatureWhenPresent() { // given diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java index e624fa56a8c..db7542c3b32 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/CachedAPIClientTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.prebid.server.execution.timeout.Timeout; @@ -13,6 +14,8 @@ import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.hooks.modules.optable.targeting.v1.core.Cache; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; @@ -170,4 +173,90 @@ public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { assertThat(result.getAudience()).isNull(); verify(cache, times(1)).put(any(), eq(targetingResult.result()), anyInt()); } + + @Test + public void shouldIncludeHidInCacheKey() { + // given + final Query query = Query.of("&id=e%3Aemail", "&hid=c:234", "&gdpr=1", ""); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).contains(URLEncoder.encode("&hid=c:234", StandardCharsets.UTF_8)); + } + + @Test + public void shouldIncludeHidAttributesInCacheKey() { + // given + final Query query = Query.of("&id=e%3Aemail", "", "&gdpr=1", "&bundle=com.example.app"); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).contains(URLEncoder.encode("&bundle=com.example.app", StandardCharsets.UTF_8)); + } + + @Test + public void shouldBuildCacheKeyWithAllComponents() { + // given + final Query query = Query.of("&id=e%3Aemail", "&hid=c:234", "&gdpr=1", "&id5_signature=sig"); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties("key", "accountId", "origin", true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).isEqualTo( + "accountId:origin:8.8.8.8:" + + URLEncoder.encode("&id=e%3Aemail", StandardCharsets.UTF_8) + + ":" + + URLEncoder.encode("&hid=c:234", StandardCharsets.UTF_8) + + ":" + + URLEncoder.encode("&id5_signature=sig", StandardCharsets.UTF_8)); + } + + @Test + public void shouldUseNullInCacheKeyWhenHidIsNull() { + // given + final Query query = Query.of("&id=e%3Aemail", null, "&gdpr=1", null); + final ArgumentCaptor keyCaptor = ArgumentCaptor.forClass(String.class); + when(cache.get(keyCaptor.capture())).thenReturn(Future.succeededFuture(givenTargetingResult())); + + // when + target.getTargeting( + givenOptableTargetingProperties(true), + query, + List.of("8.8.8.8"), + "user agent", + timeout); + + // then + final String key = keyCaptor.getValue(); + assertThat(key).endsWith(":null:null"); + } } From b8441d512aeec068a2d5cbca68e3c248556bc0ea Mon Sep 17 00:00:00 2001 From: Eugene Dorfman Date: Wed, 29 Jul 2026 12:28:12 +0200 Subject: [PATCH 15/15] optable-targeting: Replace em-dashes in README --- extra/modules/optable-targeting/README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/extra/modules/optable-targeting/README.md b/extra/modules/optable-targeting/README.md index 1dae9829156..586627d1288 100644 --- a/extra/modules/optable-targeting/README.md +++ b/extra/modules/optable-targeting/README.md @@ -295,20 +295,20 @@ the Targeting API: For app traffic (requests with an `app` object) the module forwards the application's bundle identifier as the `bundle=` query parameter and, when available, its version as `ver=`. Both values are URL-encoded. The `ver` parameter -is only sent when `bundle` is present and non-empty — requests with a version but no bundle will not produce either +is only sent when `bundle` is present and non-empty; requests with a version but no bundle will not produce either parameter. ### ID5 signature The ID5 signature is propagated to the Targeting API in two ways: -1. **Request-side signature** — when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value +1. **Request-side signature**: when the incoming OpenRTB request carries `user.ext.optable.id5_signature`, that value is sent to the Targeting API as the `id5_signature=` query parameter. -2. **Response-side signature** — when the Targeting API response contains refs with an ID5 signature, the module +2. **Response-side signature**: when the Targeting API response contains refs with an ID5 signature, the module resolves the signature through the matching optable-eid and propagates it back into the request context, where it is later injected into the bid response (`ext.prebid.passthrough.optable.id5_signature`) for downstream use. -The `id5_signature` field is also part of the Optable input erasure — see above. +The `id5_signature` field is also part of the Optable input erasure, see above. ## Analytics Tags