From 906edcc89ed4f273d738bc85ad66f7cecb643f1b Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sat, 20 Sep 2025 16:13:16 +0200 Subject: [PATCH 1/6] optable-targeting: Add User-Agent header to API call --- .../targeting/model/OptableAttributes.java | 2 ++ .../v1/core/OptableAttributesResolver.java | 8 ++++++- .../targeting/v1/core/OptableTargeting.java | 2 +- .../optable/targeting/v1/net/APIClient.java | 1 + .../targeting/v1/net/APIClientImpl.java | 8 +++++-- .../targeting/v1/net/CachedAPIClient.java | 5 ++-- .../v1/core/OptableTargetingTest.java | 8 +++---- .../targeting/v1/net/APIClientImplTest.java | 24 ++++++++++++++----- .../targeting/v1/net/CachedAPIClientTest.java | 17 ++++++++----- 9 files changed, 53 insertions(+), 22 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 5498457dcbb..9dacd6de322 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 @@ -20,5 +20,7 @@ public class OptableAttributes { List ips; + String userAgent; + Long timeout; } 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 6fda659278f..7f2aad0657d 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 @@ -22,6 +22,7 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, final OptableAttributes.OptableAttributesBuilder builder = OptableAttributes.builder() .ips(resolveIp(auctionContext)) + .userAgent(resolveUserAgent(auctionContext)) .timeout(timeout); if (tcfContext.isConsentValid()) { @@ -39,7 +40,12 @@ public static OptableAttributes resolveAttributes(AuctionContext auctionContext, return builder.build(); } - public static List resolveIp(AuctionContext auctionContext) { + public static String resolveUserAgent(AuctionContext auctionContext) { + final Device device = auctionContext.getBidRequest().getDevice(); + return device != null ? device.getUa() : null; + } + + private static List resolveIp(AuctionContext auctionContext) { final List result = new ArrayList<>(); final Optional deviceOpt = Optional.ofNullable(auctionContext.getBidRequest().getDevice()); 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 c45ce8b6f9f..0cb16d9c456 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 @@ -34,6 +34,6 @@ public Future getTargeting(OptableTargetingProperties propertie return Future.failedFuture("Can't get targeting"); } - return apiClient.getTargeting(properties, query, attributes.getIps(), timeout); + return apiClient.getTargeting(properties, query, attributes.getIps(), attributes.getUserAgent(), timeout); } } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java index 9e0a4f3db17..ecb96d39cda 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClient.java @@ -13,5 +13,6 @@ public interface APIClient { Future getTargeting(OptableTargetingProperties properties, Query query, List ips, + String userAgent, Timeout timeout); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java index 436ad729375..d31929f111f 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImpl.java @@ -49,11 +49,12 @@ public APIClientImpl(String endpoint, public Future getTargeting(OptableTargetingProperties properties, Query query, List ips, + String userAgent, Timeout timeout) { final String uri = resolveEndpoint(properties.getTenant(), properties.getOrigin()); final String queryAsString = query.toQueryString(); - final MultiMap headers = headers(properties, ips); + final MultiMap headers = headers(properties, ips, userAgent); return httpClient.get(uri + queryAsString, headers, timeout.remaining()) .compose(this::validateResponse) @@ -67,10 +68,13 @@ private String resolveEndpoint(String tenant, String origin) { .replace(ORIGIN, origin); } - private static MultiMap headers(OptableTargetingProperties properties, List ips) { + private static MultiMap headers(OptableTargetingProperties properties, List ips, String userAgent) { final MultiMap headers = HeadersMultiMap.headers() .add(HttpUtil.ACCEPT_HEADER, "application/json"); + if (userAgent != null) { + headers.add(HttpUtil.USER_AGENT_HEADER, userAgent); + } final String apiKey = properties.getApiKey(); if (StringUtils.isNotEmpty(apiKey)) { headers.add(HttpUtil.AUTHORIZATION_HEADER, "Bearer %s".formatted(apiKey)); 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 73ade87e5e8..e7e8bc3e452 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 @@ -28,18 +28,19 @@ public CachedAPIClient(APIClient apiClient, Cache cache, boolean isCircuitBreake public Future getTargeting(OptableTargetingProperties properties, Query query, List ips, + String userAgent, Timeout timeout) { final CacheProperties cacheProperties = properties.getCache(); if (!cacheProperties.isEnabled()) { - return apiClient.getTargeting(properties, query, ips, timeout); + return apiClient.getTargeting(properties, query, ips, userAgent, timeout); } final String tenant = properties.getTenant(); final String origin = properties.getOrigin(); return cache.get(createCachingKey(tenant, origin, ips, query, true)) - .recover(ignore -> apiClient.getTargeting(properties, query, ips, timeout) + .recover(ignore -> apiClient.getTargeting(properties, query, ips, userAgent, timeout) .recover(throwable -> isCircuitBreakerEnabled ? Future.succeededFuture(new TargetingResult(null, null)) : Future.failedFuture(throwable)) diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java index 9f793605f12..4533ef073ae 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/OptableTargetingTest.java @@ -51,7 +51,7 @@ public void setUp() { public void shouldCallNonCachedAPIClient() { // given when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); - when(apiClient.getTargeting(any(), any(), any(), any())) + when(apiClient.getTargeting(any(), any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); final BidRequest bidRequest = givenBidRequest(); @@ -64,7 +64,7 @@ public void shouldCallNonCachedAPIClient() { // then assertThat(targetingResult.result()).isNotNull(); - verify(apiClient).getTargeting(any(), any(), any(), any()); + verify(apiClient).getTargeting(any(), any(), any(), any(), any()); } @Test @@ -72,7 +72,7 @@ public void shouldUseCachedAPIClient() { // given when(idsMapper.toIds(any(), any())).thenReturn(List.of(Id.of(Id.ID5, "id"))); when(cache.get(any())).thenReturn(Future.failedFuture(new NullPointerException())); - when(apiClient.getTargeting(any(), any(), any(), any())) + when(apiClient.getTargeting(any(), any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); final BidRequest bidRequest = givenBidRequest(); @@ -84,7 +84,7 @@ public void shouldUseCachedAPIClient() { // then verify(cache).get(any()); - verify(apiClient).getTargeting(any(), any(), any(), any()); + verify(apiClient).getTargeting(any(), any(), any(), any(), any()); } private OptableAttributes givenOptableAttributes() { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java index 64c23c541fc..d147d7d4294 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/net/APIClientImplTest.java @@ -54,6 +54,7 @@ public void shouldReturnTargetingResult() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), + "user agent", timeout); // then @@ -75,6 +76,7 @@ public void shouldReturnNullWhenEndpointRespondsWithError() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), + "user agent", timeout); // then @@ -92,6 +94,7 @@ public void shouldNotFailWhenEndpointRespondsWithWrongData() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), + "user agent", timeout); // then @@ -109,6 +112,7 @@ public void shouldNotFailWhenHttpClientIsCrashed() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), + "user agent", timeout); // then @@ -126,6 +130,7 @@ public void shouldNotFailWhenInternalErrorOccurs() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8"), + "user agent", timeout); // then @@ -143,7 +148,7 @@ public void shouldUseAuthorizationHeaderIfApiKeyIsPresent() { // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + givenQuery(), List.of("8.8.8.8"), "user agent", timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); @@ -168,7 +173,7 @@ public void shouldBuildApiUrlByReplacingTenantAndOriginMacros() { // when final Future result = target.getTargeting(givenOptableTargetingProperties(false), - givenQuery(), List.of("8.8.8.8"), timeout); + givenQuery(), List.of("8.8.8.8"), "user agent", timeout); // then final ArgumentCaptor endpointCaptor = ArgumentCaptor.forClass(String.class); @@ -189,6 +194,7 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { givenOptableTargetingProperties(null, false), givenQuery(), List.of("8.8.8.8"), + "user agent", timeout); // then @@ -200,7 +206,7 @@ public void shouldNotUseAuthorizationHeaderIfApiKeyIsAbsent() { } @Test - public void shouldPassThroughIpAddresses() { + public void shouldPassThroughIpAddressesAndUserAgent() { // given when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); @@ -210,18 +216,21 @@ public void shouldPassThroughIpAddresses() { givenOptableTargetingProperties(false), givenQuery(), List.of("8.8.8.8", "2001:4860:4860::8888"), + "user agent", timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); - assertThat(headersCaptor.getValue().getAll(HttpUtil.X_FORWARDED_FOR_HEADER)) + final MultiMap headers = headersCaptor.getValue(); + assertThat(headers.getAll(HttpUtil.X_FORWARDED_FOR_HEADER)) .contains("8.8.8.8", "2001:4860:4860::8888"); + assertThat(headers.get(HttpUtil.USER_AGENT_HEADER)).isEqualTo("user agent"); assertThat(result.result()).isNull(); } @Test - public void shouldNotPassThroughIpAddressWhenNotSpecified() { + public void shouldNotPassThroughIpAddressAndUserAgentWhenNotSpecified() { // given when(httpClient.get(any(), any(), anyLong())).thenReturn(Future.succeededFuture( givenFailHttpResponse(HttpStatus.SC_INTERNAL_SERVER_ERROR, "plain_text_response.json"))); @@ -231,12 +240,15 @@ public void shouldNotPassThroughIpAddressWhenNotSpecified() { givenOptableTargetingProperties(false), givenQuery(), null, + null, timeout); // then final ArgumentCaptor headersCaptor = ArgumentCaptor.forClass(MultiMap.class); verify(httpClient).get(any(), headersCaptor.capture(), anyLong()); - assertThat(headersCaptor.getValue().get(HttpUtil.X_FORWARDED_FOR_HEADER)).isNull(); + final MultiMap headers = headersCaptor.getValue(); + assertThat(headers.get(HttpUtil.X_FORWARDED_FOR_HEADER)).isNull(); + assertThat(headers.get(HttpUtil.USER_AGENT_HEADER)).isNull(); assertThat(result.result()).isNull(); } } 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 6c365f2c2c0..e624fa56a8c 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 @@ -49,7 +49,7 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { when(cache.get(any())).thenReturn(Future.failedFuture("error")); when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final Query query = givenQuery(); - when(apiClient.getTargeting(any(), any(), any(), any())) + when(apiClient.getTargeting(any(), any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -57,6 +57,7 @@ public void shouldCallAPIAndAddTargetingResultsToCache() { givenOptableTargetingProperties(true), query, List.of("8.8.8.8"), + "user agent", timeout); // then @@ -75,7 +76,7 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() when(cache.get(any())).thenReturn(Future.failedFuture(new IllegalArgumentException("message"))); when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); final Query query = givenQuery(); - when(apiClient.getTargeting(any(), any(), any(), any())) + when(apiClient.getTargeting(any(), any(), any(), any(), any())) .thenReturn(Future.succeededFuture(givenTargetingResult())); // when @@ -83,6 +84,7 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() givenOptableTargetingProperties(true), query, List.of("8.8.8.8"), + "user agent", timeout); // then @@ -92,7 +94,7 @@ public void shouldCallAPIAndAddTargetingResultsToCacheWhenCacheReturnsFailure() .returns("id", it -> it.getEids().getFirst().getUids().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); - verify(apiClient, times(1)).getTargeting(any(), any(), any(), any()); + verify(apiClient, times(1)).getTargeting(any(), any(), any(), any(), any()); verify(cache).put(any(), eq(targetingResult.result()), anyInt()); } @@ -107,6 +109,7 @@ public void shouldUseCachedResult() { givenOptableTargetingProperties(true), query, List.of("8.8.8.8"), + "user agent", timeout); // then @@ -117,7 +120,7 @@ public void shouldUseCachedResult() { .returns("id", it -> it.getData().getFirst().getId()) .returns("id", it -> it.getData().getFirst().getSegment().getFirst().getId()); verify(cache, times(1)).get(any()); - verify(apiClient, times(0)).getTargeting(any(), any(), any(), any()); + verify(apiClient, times(0)).getTargeting(any(), any(), any(), any(), any()); verify(cache, times(0)).put(any(), eq(targetingResult.result()), anyInt()); } @@ -126,7 +129,7 @@ public void shouldNotFailWhenApiClientIsFailed() { // given final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); - when(apiClient.getTargeting(any(), any(), any(), any())) + when(apiClient.getTargeting(any(), any(), any(), any(), any())) .thenReturn(Future.failedFuture(new NullPointerException())); // when @@ -134,6 +137,7 @@ public void shouldNotFailWhenApiClientIsFailed() { givenOptableTargetingProperties(true), query, List.of("8.8.8.8"), + "user agent", timeout); // then @@ -146,7 +150,7 @@ public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { // given final Query query = givenQuery(); when(cache.get(any())).thenReturn(Future.failedFuture("empty")); - when(apiClient.getTargeting(any(), any(), any(), any())) + when(apiClient.getTargeting(any(), any(), any(), any(), any())) .thenReturn(Future.failedFuture(new NullPointerException())); when(cache.put(any(), any(), anyInt())).thenReturn(Future.succeededFuture()); @@ -156,6 +160,7 @@ public void shouldCacheEmptyResultWhenCircuitBreakerIsOn() { givenOptableTargetingProperties(true), query, List.of("8.8.8.8"), + "user agent", timeout); // then From f4b8e43144312d7cc112dfa2cd9580931427c010 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Sat, 20 Sep 2025 16:53:31 +0200 Subject: [PATCH 2/6] optable-targeting: Add prebid-server signature to API query --- .../optable/targeting/model/OptableAttributes.java | 6 ++++++ .../modules/optable/targeting/v1/core/QueryBuilder.java | 2 ++ .../optable/targeting/v1/core/QueryBuilderTest.java | 8 ++++---- 3 files changed, 12 insertions(+), 4 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 9dacd6de322..e4b055ed867 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 @@ -10,6 +10,8 @@ @Builder(toBuilder = true) public class OptableAttributes { + private static final String REQUEST_SOURCE = "prebid-server"; + String gpp; Set gppSid; @@ -23,4 +25,8 @@ public class OptableAttributes { String userAgent; Long timeout; + + public String getRequestSource() { + return REQUEST_SOURCE; + } } 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 2cf964bf63b..16eae990080 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 @@ -81,6 +81,8 @@ private static String buildAttributesString(OptableAttributes optableAttributes) Optional.ofNullable(optableAttributes.getTimeout()) .ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms")); + sb.append("&osdk=").append(optableAttributes.getRequestSource()); + return sb.toString(); } } 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 606c76db4a2..0359578ee75 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 @@ -25,7 +25,7 @@ public void shouldSeparateAttributesFromIds() { // then assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); - assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms"); + assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); } @Test @@ -38,9 +38,9 @@ public void shouldBuildFullQueryString() { // then assertThat(query.getIds()).isEqualTo("&id=e%3Aemail&id=p%3A123"); - assertThat(query.getAttributes()).isEqualTo("&gdpr_consent=tcf&gdpr=1&timeout=100ms"); + 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"); + .isEqualTo("&id=e%3Aemail&id=p%3A123&gdpr_consent=tcf&gdpr=1&timeout=100ms&osdk=prebid-server"); } @Test @@ -96,7 +96,7 @@ public void shouldBuildQueryStringWhenIdsListIsEmptyAndIpIsPresent() { // then assertThat(query).isNotNull(); - assertThat(query.toQueryString()).isEqualTo("&gdpr=0"); + assertThat(query.toQueryString()).isEqualTo("&gdpr=0&osdk=prebid-server"); } @Test From 79feabe571d0c787dd44a309414dd27b4bf1771c Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 25 Sep 2025 16:00:45 +0200 Subject: [PATCH 3/6] optable-targeting: Conditional Optable EIDS merging in case requests were already enriched through web client --- .../config/OptableTargetingProperties.java | 10 + ...eTargetingProcessedAuctionRequestHook.java | 7 +- .../targeting/v1/core/BidRequestEnricher.java | 89 +++++++-- .../v1/core/BidRequestEnricherTest.java | 183 +++++++++++++++--- 4 files changed, 246 insertions(+), 43 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index c13bf132ca9..68fa889575f 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 @@ -4,6 +4,7 @@ import lombok.Data; import lombok.NoArgsConstructor; +import java.util.List; import java.util.Map; @Data @@ -31,5 +32,14 @@ public final class OptableTargetingProperties { @JsonProperty("id-prefix-order") String idPrefixOrder; + @JsonProperty("no-skip") + List noSkip; + + @JsonProperty("optable-inserter-eids-merge") + List optableInserterEidsMerge = List.of(); + + @JsonProperty("optable-inserter-eids-replace") + List optableInserterEidsReplace = List.of(); + CacheProperties cache = new CacheProperties(); } 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 2984d3c90b7..a5ad2559d40 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 @@ -89,7 +89,7 @@ public Future> call(AuctionRequestPayloa .compose(targetingResult -> { moduleContext.setOptableTargetingExecutionTime( System.currentTimeMillis() - callTargetingAPITimestamp); - return enrichedPayload(targetingResult, moduleContext); + return enrichedPayload(targetingResult, moduleContext, properties); }) .recover(throwable -> { moduleContext.setOptableTargetingExecutionTime( @@ -143,13 +143,14 @@ private Timeout getHookTimeout(AuctionInvocationContext invocationContext) { } private Future> enrichedPayload(TargetingResult targetingResult, - ModuleContext moduleContext) { + ModuleContext moduleContext, + OptableTargetingProperties properties) { moduleContext.setTargeting(targetingResult.getAudience()); moduleContext.setEnrichRequestStatus(EnrichmentStatus.success()); return update( BidRequestCleaner.instance() - .andThen(BidRequestEnricher.of(targetingResult)) + .andThen(BidRequestEnricher.of(targetingResult, properties)) ::apply, moduleContext); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index b3ea3e6dce9..8236fc89ee0 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -4,13 +4,16 @@ import com.iab.openrtb.request.Data; import com.iab.openrtb.request.Eid; import com.iab.openrtb.request.Segment; +import com.iab.openrtb.request.Uid; import org.apache.commons.collections4.CollectionUtils; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; +import org.prebid.server.util.ObjectUtil; import java.util.HashMap; import java.util.List; @@ -23,14 +26,18 @@ public class BidRequestEnricher implements PayloadUpdate { + private static final String OPTABLE_CO_INSERTER = "optable.co"; + private final TargetingResult targetingResult; + private final OptableTargetingProperties targetingProperties; - private BidRequestEnricher(TargetingResult targetingResult) { + private BidRequestEnricher(TargetingResult targetingResult, OptableTargetingProperties targetingProperties) { this.targetingResult = targetingResult; + this.targetingProperties = targetingProperties; } - public static BidRequestEnricher of(TargetingResult targetingResult) { - return new BidRequestEnricher(targetingResult); + public static BidRequestEnricher of(TargetingResult targetingResult, OptableTargetingProperties properties) { + return new BidRequestEnricher(targetingResult, properties); } @Override @@ -56,22 +63,82 @@ private BidRequest enrichBidRequest(BidRequest bidRequest) { .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); return bidRequest.toBuilder() - .user(mergeUserData(bidRequestUser, optableUser)) + .user(mergeUserData(bidRequestUser, optableUser, targetingProperties)) .build(); } - private static com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { + private static com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, + User optableUser, + OptableTargetingProperties targetingProperties) { + return user.toBuilder() - .eids(mergeEids(user.getEids(), optableUser.getEids())) + .eids(mergeEids(user.getEids(), optableUser.getEids(), targetingProperties)) .data(mergeData(user.getData(), optableUser.getData())) .build(); } - private static List mergeEids(List destination, List source) { - return merge( - destination, - source, - Eid::getSource); + private static List mergeEids(List destination, + List source, + OptableTargetingProperties targetingProperties) { + + if (CollectionUtils.isEmpty(destination)) { + return source; + } + + if (CollectionUtils.isEmpty(source)) { + return destination; + } + + final Map idToSourceEid = source.stream().collect(Collectors.toMap( + BidRequestEnricher::eidIdExtractor, + Function.identity(), + (a, b) -> b, + HashMap::new)); + + final List sourceToReplace = targetingProperties.getOptableInserterEidsReplace(); + final List sourceToMerge = targetingProperties.getOptableInserterEidsMerge() + .stream() + .filter(it -> !sourceToReplace.contains(it)).toList(); + + final List mergedEid = destination.stream() + .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid)) + && destinationEid.getInserter().equals(OPTABLE_CO_INSERTER) + ? resolveEidConflict( + destinationEid, + idToSourceEid.get(eidIdExtractor(destinationEid)), + sourceToMerge, + sourceToReplace) + : destinationEid) + .toList(); + + return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor); + } + + private static Eid resolveEidConflict(Eid destinationEid, Eid sourceEid, List sourceToMerge, + List sourceToReplace) { + + final String eidSource = sourceEid.getSource(); + + if (sourceToReplace.contains(eidSource)) { + return sourceEid; + } + if (sourceToMerge.contains(eidSource)) { + return mergeEid(destinationEid, sourceEid); + } + + return destinationEid; + } + + private static Eid mergeEid(Eid destinationEid, Eid sourceEid) { + return destinationEid.toBuilder() + .uids(merge(destinationEid.getUids(), sourceEid.getUids(), Uid::getId)) + .build(); + } + + private static String eidIdExtractor(Eid eid) { + return "%s_%s".formatted( + ObjectUtil.getIfNotNullOrDefault(eid, Eid::getInserter, () -> ""), + eid.getSource()); } private static List mergeData(List destination, List source) { diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java index 038a0958acc..2c2ff566aa8 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -7,8 +7,10 @@ import com.iab.openrtb.request.Segment; import com.iab.openrtb.request.Uid; import com.iab.openrtb.request.User; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; +import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.TargetingResult; import org.prebid.server.hooks.modules.optable.targeting.v1.BaseOptableTest; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; @@ -16,17 +18,20 @@ import java.util.Collections; import java.util.List; +import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; public class BidRequestEnricherTest extends BaseOptableTest { + private final OptableTargetingProperties targetingProperties = new OptableTargetingProperties(); + @Test public void shouldReturnOriginBidRequestWhenNoTargetingResults() { // given final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(givenBidRequest()); // when - final AuctionRequestPayload result = BidRequestEnricher.of(null) + final AuctionRequestPayload result = BidRequestEnricher.of(null, targetingProperties) .apply(auctionRequestPayload); // then @@ -44,7 +49,7 @@ public void shouldNotFailIfBidRequestIsNull() { final TargetingResult targetingResult = givenTargetingResult(); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -58,7 +63,7 @@ public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { final TargetingResult targetingResult = givenTargetingResult(); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -73,15 +78,15 @@ public void shouldReturnEnrichedBidRequestWhenTargetingResultsIsPresent() { public void shouldNotAddEidWhenSourceAlreadyPresent() { // given final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source", List.of(givenUid("id2", 3, null)), null))); + givenEid("inserter", "source", List.of(givenUid("id2", 3, null)), null))); final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( - givenEid("source", List.of(givenUid("id", null, null)), null), - givenEid("source1", List.of(givenUid("id", null, null)), null))); + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source1", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -95,15 +100,15 @@ public void shouldNotAddEidWhenSourceAlreadyPresent() { public void shouldAddEidWhenSourceIsNotAlreadyPresent() { // given final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source3", List.of(givenUid("id2", 3, null)), null))); + givenEid("inserter", "source3", List.of(givenUid("id2", 3, null)), null))); final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( - givenEid("source1", List.of(givenUid("id", null, null)), null), - givenEid("source2", List.of(givenUid("id", null, null)), null))); + givenEid("inserter", "source1", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source2", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -113,19 +118,138 @@ public void shouldAddEidWhenSourceIsNotAlreadyPresent() { assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2", "source3"); } + @Test + public void shouldSkipEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .containsExactly("id"); + } + + @Test + public void shouldMergeEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsMerge(List.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .contains("id", "id2"); + } + + @Test + public void shouldReplaceEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsReplace(List.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .containsExactly("id2"); + } + + @Test + public void shouldReplaceEidWhenOptableSourceIsPresentInBothMergeAndReplaceLists() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsReplace(List.of("source2")); + properties.setOptableInserterEidsMerge(List.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(2); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1", "source2"); + assertThat(eids) + .filteredOn(eid -> "source2".equals(eid.getSource())) + .singleElement() + .extracting(Eid::getUids, as(InstanceOfAssertFactories.list(Uid.class))) + .extracting(Uid::getId) + .containsExactly("id2"); + } + @Test public void shouldNotMergeOriginEidsWithTheSameSource() { // given final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source3", List.of(givenUid("id2", 3, null)), null))); + givenEid("inserter", "source3", List.of(givenUid("id2", 3, null)), null))); final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( - givenEid("source", List.of(givenUid("id", null, null)), null), - givenEid("source", List.of(givenUid("id", null, null)), null))); + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -139,13 +263,13 @@ public void shouldNotMergeOriginEidsWithTheSameSource() { public void shouldApplyOriginEidsWhenTargetingIsEmpty() { // given final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( - givenEid("source3", List.of(givenUid("id2", 3, null)), null))); + givenEid("inserter", "source3", List.of(givenUid("id2", 3, null)), null))); final BidRequest bidRequest = givenBidRequestWithUserEids(Collections.emptyList()); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -161,12 +285,12 @@ public void shouldApplyTargetingEidsWhenOriginListIsEmpty() { final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( - givenEid("source", List.of(givenUid("id", null, null)), null), - givenEid("source1", List.of(givenUid("id", null, null)), null))); + givenEid("inserter", "source", List.of(givenUid("id", null, null)), null), + givenEid("inserter", "source1", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -184,7 +308,7 @@ public void shouldNotApplyEidsWhenOriginAndTargetingEidsAreEmpty() { final TargetingResult targetingResult = givenTargetingResultWithEids(Collections.emptyList()); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -203,7 +327,7 @@ public void shouldMergeDataWithTheSameId() { givenData("id", List.of(givenSegment("id3", "value3"))))); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -228,7 +352,7 @@ public void shouldMergeDistinctSegmentsWithinTheSameData() { givenData("id", List.of(givenSegment("id4", "value4"))))); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -254,7 +378,7 @@ public void shouldAppendDataWithNewId() { givenData("id1", List.of(givenSegment("id3", "value3"))))); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -278,7 +402,7 @@ public void shouldApplyOriginDataWhenTargetingIsEmpty() { final TargetingResult targetingResult = givenTargetingResultWithData(Collections.emptyList()); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -299,7 +423,7 @@ public void shouldApplyTargetingDataWhenOriginIsEmpty() { givenData("id", List.of(givenSegment("id1", "value1"))))); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -316,7 +440,7 @@ public void shouldApplyNothingWhenOriginAndTargetingDataAreEmpty() { final TargetingResult targetingResult = givenTargetingResultWithData(Collections.emptyList()); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -331,7 +455,7 @@ public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { final TargetingResult targetingResult = givenEmptyTargetingResult(); // when - final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult) + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, targetingProperties) .apply(auctionRequestPayload); // then @@ -342,8 +466,9 @@ public void shouldReturnOriginBidRequestWhenTargetingResultsIsEmpty() { assertThat(user.getData()).isNull(); } - private Eid givenEid(String source, List uids, ObjectNode ext) { + private Eid givenEid(String inserter, String source, List uids, ObjectNode ext) { return Eid.builder() + .inserter(inserter) .source(source) .uids(uids) .ext(ext) From 0f487b65ef49dfba13776df9ebe33e274dfd3d67 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 29 Sep 2025 19:25:06 +0200 Subject: [PATCH 4/6] optable-targeting: Code cleanup --- .../targeting/model/OptableAttributes.java | 6 ---- .../config/OptableTargetingProperties.java | 9 ++---- .../targeting/v1/core/BidRequestEnricher.java | 32 ++++++++----------- .../targeting/v1/core/QueryBuilder.java | 4 ++- .../v1/core/BidRequestEnricherTest.java | 9 +++--- 5 files changed, 25 insertions(+), 35 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 e4b055ed867..9dacd6de322 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 @@ -10,8 +10,6 @@ @Builder(toBuilder = true) public class OptableAttributes { - private static final String REQUEST_SOURCE = "prebid-server"; - String gpp; Set gppSid; @@ -25,8 +23,4 @@ public class OptableAttributes { String userAgent; Long timeout; - - public String getRequestSource() { - return REQUEST_SOURCE; - } } 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 68fa889575f..31843b3138a 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 @@ -4,8 +4,8 @@ import lombok.Data; import lombok.NoArgsConstructor; -import java.util.List; import java.util.Map; +import java.util.Set; @Data @NoArgsConstructor @@ -32,14 +32,11 @@ public final class OptableTargetingProperties { @JsonProperty("id-prefix-order") String idPrefixOrder; - @JsonProperty("no-skip") - List noSkip; - @JsonProperty("optable-inserter-eids-merge") - List optableInserterEidsMerge = List.of(); + Set optableInserterEidsMerge = Set.of(); @JsonProperty("optable-inserter-eids-replace") - List optableInserterEidsReplace = List.of(); + Set optableInserterEidsReplace = Set.of(); CacheProperties cache = new CacheProperties(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index 8236fc89ee0..7fdfa213d88 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -13,7 +13,6 @@ import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.User; import org.prebid.server.hooks.v1.PayloadUpdate; import org.prebid.server.hooks.v1.auction.AuctionRequestPayload; -import org.prebid.server.util.ObjectUtil; import java.util.HashMap; import java.util.List; @@ -63,23 +62,19 @@ private BidRequest enrichBidRequest(BidRequest bidRequest) { .orElseGet(() -> com.iab.openrtb.request.User.builder().build()); return bidRequest.toBuilder() - .user(mergeUserData(bidRequestUser, optableUser, targetingProperties)) + .user(mergeUserData(bidRequestUser, optableUser)) .build(); } - private static com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, - User optableUser, - OptableTargetingProperties targetingProperties) { + private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { return user.toBuilder() - .eids(mergeEids(user.getEids(), optableUser.getEids(), targetingProperties)) + .eids(mergeEids(user.getEids(), optableUser.getEids())) .data(mergeData(user.getData(), optableUser.getData())) .build(); } - private static List mergeEids(List destination, - List source, - OptableTargetingProperties targetingProperties) { + private List mergeEids(List destination, List source) { if (CollectionUtils.isEmpty(destination)) { return source; @@ -95,14 +90,14 @@ private static List mergeEids(List destination, (a, b) -> b, HashMap::new)); - final List sourceToReplace = targetingProperties.getOptableInserterEidsReplace(); - final List sourceToMerge = targetingProperties.getOptableInserterEidsMerge() + final Set sourceToReplace = targetingProperties.getOptableInserterEidsReplace(); + final Set sourceToMerge = targetingProperties.getOptableInserterEidsMerge() .stream() - .filter(it -> !sourceToReplace.contains(it)).toList(); + .filter(it -> !sourceToReplace.contains(it)).collect(Collectors.toSet()); final List mergedEid = destination.stream() .map(destinationEid -> idToSourceEid.containsKey(eidIdExtractor(destinationEid)) - && destinationEid.getInserter().equals(OPTABLE_CO_INSERTER) + && OPTABLE_CO_INSERTER.equals(destinationEid.getInserter()) ? resolveEidConflict( destinationEid, idToSourceEid.get(eidIdExtractor(destinationEid)), @@ -114,8 +109,10 @@ private static List mergeEids(List destination, return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor); } - private static Eid resolveEidConflict(Eid destinationEid, Eid sourceEid, List sourceToMerge, - List sourceToReplace) { + private static Eid resolveEidConflict(Eid destinationEid, + Eid sourceEid, + Set sourceToMerge, + Set sourceToReplace) { final String eidSource = sourceEid.getSource(); @@ -136,9 +133,8 @@ private static Eid mergeEid(Eid destinationEid, Eid sourceEid) { } private static String eidIdExtractor(Eid eid) { - return "%s_%s".formatted( - ObjectUtil.getIfNotNullOrDefault(eid, Eid::getInserter, () -> ""), - eid.getSource()); + final String inserter = Optional.ofNullable(eid.getInserter()).orElse(""); + return "%s_%s".formatted(inserter, eid.getSource()); } private static List mergeData(List destination, List source) { 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 16eae990080..613286f4b92 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 @@ -21,6 +21,8 @@ public class QueryBuilder { + private static final String REQUEST_SOURCE = "prebid-server"; + private QueryBuilder() { } @@ -81,7 +83,7 @@ private static String buildAttributesString(OptableAttributes optableAttributes) Optional.ofNullable(optableAttributes.getTimeout()) .ifPresent(timeout -> sb.append("&timeout=").append(timeout).append("ms")); - sb.append("&osdk=").append(optableAttributes.getRequestSource()); + sb.append("&osdk=").append(REQUEST_SOURCE); return sb.toString(); } diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java index 2c2ff566aa8..24fd5cb98f1 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -17,6 +17,7 @@ import java.util.Collections; import java.util.List; +import java.util.Set; import static org.assertj.core.api.Assertions.as; import static org.assertj.core.api.Assertions.assertThat; @@ -157,7 +158,7 @@ public void shouldMergeEidWhenOptableSourceIsAlreadyPresent() { givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); final OptableTargetingProperties properties = new OptableTargetingProperties(); - properties.setOptableInserterEidsMerge(List.of("source2")); + properties.setOptableInserterEidsMerge(Set.of("source2")); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) @@ -187,7 +188,7 @@ public void shouldReplaceEidWhenOptableSourceIsAlreadyPresent() { givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); final OptableTargetingProperties properties = new OptableTargetingProperties(); - properties.setOptableInserterEidsReplace(List.of("source2")); + properties.setOptableInserterEidsReplace(Set.of("source2")); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) @@ -217,8 +218,8 @@ public void shouldReplaceEidWhenOptableSourceIsPresentInBothMergeAndReplaceLists givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); final OptableTargetingProperties properties = new OptableTargetingProperties(); - properties.setOptableInserterEidsReplace(List.of("source2")); - properties.setOptableInserterEidsMerge(List.of("source2")); + properties.setOptableInserterEidsReplace(Set.of("source2")); + properties.setOptableInserterEidsMerge(Set.of("source2")); // when final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) From bf902bb6997ce6ada09c3c24db82058cf1df341c Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Thu, 2 Oct 2025 21:52:08 +0200 Subject: [PATCH 5/6] optable-targeting: Conditional EIDs merging. Add Optable EIDs filter by source --- .../config/OptableTargetingProperties.java | 3 ++ .../targeting/v1/core/BidRequestEnricher.java | 20 ++++++-- .../v1/core/BidRequestEnricherTest.java | 47 +++++++++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/model/config/OptableTargetingProperties.java index 31843b3138a..7f0598da83e 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 @@ -38,5 +38,8 @@ public final class OptableTargetingProperties { @JsonProperty("optable-inserter-eids-replace") Set optableInserterEidsReplace = Set.of(); + @JsonProperty("optable-inserter-eids-ignore") + Set optableInserterEidsIgnore = Set.of(); + CacheProperties cache = new CacheProperties(); } diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index 7fdfa213d88..39765a1573b 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -67,15 +67,13 @@ private BidRequest enrichBidRequest(BidRequest bidRequest) { } private com.iab.openrtb.request.User mergeUserData(com.iab.openrtb.request.User user, User optableUser) { - return user.toBuilder() - .eids(mergeEids(user.getEids(), optableUser.getEids())) + .eids(filterOptableEids(mergeEids(user.getEids(), optableUser.getEids()))) .data(mergeData(user.getData(), optableUser.getData())) .build(); } private List mergeEids(List destination, List source) { - if (CollectionUtils.isEmpty(destination)) { return source; } @@ -109,6 +107,22 @@ private List mergeEids(List destination, List source) { return merge(mergedEid, source, BidRequestEnricher::eidIdExtractor); } + private List filterOptableEids(List eids) { + if (CollectionUtils.isEmpty(eids)) { + return eids; + } + + final Set optableIdsToIgnore = targetingProperties.getOptableInserterEidsIgnore(); + if (CollectionUtils.isEmpty(optableIdsToIgnore)) { + return eids; + } + + return eids.stream() + .filter(eid -> !OPTABLE_CO_INSERTER.equals(eid.getInserter()) + || !optableIdsToIgnore.contains(eid.getSource())) + .toList(); + } + private static Eid resolveEidConflict(Eid destinationEid, Eid sourceEid, Set sourceToMerge, diff --git a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java index 24fd5cb98f1..71ee889008f 100644 --- a/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java +++ b/extra/modules/optable-targeting/src/test/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricherTest.java @@ -177,6 +177,53 @@ public void shouldMergeEidWhenOptableSourceIsAlreadyPresent() { .contains("id", "id2"); } + @Test + public void shouldRemoveEidWhenOptableSourceIsAlreadyPresent() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of( + givenEid("optable.co", "source2", List.of(givenUid("id2", 3, null)), null))); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsIgnore(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(1); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1"); + } + + @Test + public void shouldRemoveEidWhenOptableSourceIsAlreadyPresentAndEmptyTargeting() { + // given + final TargetingResult targetingResult = givenTargetingResultWithEids(List.of()); + + final BidRequest bidRequest = givenBidRequestWithUserEids(List.of( + givenEid("optable.co", "source1", List.of(givenUid("id", null, null)), null), + givenEid("optable.co", "source2", List.of(givenUid("id", null, null)), null))); + final AuctionRequestPayload auctionRequestPayload = AuctionRequestPayloadImpl.of(bidRequest); + final OptableTargetingProperties properties = new OptableTargetingProperties(); + properties.setOptableInserterEidsIgnore(Set.of("source2")); + + // when + final AuctionRequestPayload result = BidRequestEnricher.of(targetingResult, properties) + .apply(auctionRequestPayload); + + // then + assertThat(result.bidRequest()).isNotNull(); + final List eids = result.bidRequest().getUser().getEids(); + assertThat(eids.size()).isEqualTo(1); + assertThat(eids.stream()).extracting(Eid::getSource).containsExactly("source1"); + } + @Test public void shouldReplaceEidWhenOptableSourceIsAlreadyPresent() { // given From 54563966c798f6c7254ba2814d687f340161f0d3 Mon Sep 17 00:00:00 2001 From: softcoder594 Date: Mon, 6 Oct 2025 18:12:07 +0200 Subject: [PATCH 6/6] optable-targeting: Code cleanup --- .../modules/optable/targeting/v1/core/BidRequestEnricher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java index 39765a1573b..2a60389802c 100644 --- a/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java +++ b/extra/modules/optable-targeting/src/main/java/org/prebid/server/hooks/modules/optable/targeting/v1/core/BidRequestEnricher.java @@ -6,6 +6,7 @@ import com.iab.openrtb.request.Segment; import com.iab.openrtb.request.Uid; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.StringUtils; import org.prebid.server.hooks.execution.v1.auction.AuctionRequestPayloadImpl; import org.prebid.server.hooks.modules.optable.targeting.model.config.OptableTargetingProperties; import org.prebid.server.hooks.modules.optable.targeting.model.openrtb.Ortb2; @@ -147,8 +148,7 @@ private static Eid mergeEid(Eid destinationEid, Eid sourceEid) { } private static String eidIdExtractor(Eid eid) { - final String inserter = Optional.ofNullable(eid.getInserter()).orElse(""); - return "%s_%s".formatted(inserter, eid.getSource()); + return "%s_%s".formatted(StringUtils.defaultString(eid.getInserter()), eid.getSource()); } private static List mergeData(List destination, List source) {