From 06020dd505bfd815b125afbb1c6e236899776440 Mon Sep 17 00:00:00 2001 From: Serhii Kolomiiets Date: Fri, 17 Nov 2023 14:41:49 +0200 Subject: [PATCH] Configurable targeting prefix --- .../server/auction/BidResponseCreator.java | 29 ++++++++- .../auction/TargetingKeywordsCreator.java | 60 ++++++++++--------- .../server/auction/VideoResponseFactory.java | 10 ++-- .../server/handler/openrtb2/AmpHandler.java | 2 +- .../ext/request/ExtRequestTargeting.java | 5 ++ .../model/AccountTargetingConfig.java | 3 + .../server/validation/RequestValidator.java | 16 +++++ .../auction/TargetingKeywordsCreatorTest.java | 55 +++++++++++------ .../validation/RequestValidatorTest.java | 28 +++++++++ 9 files changed, 153 insertions(+), 55 deletions(-) diff --git a/src/main/java/org/prebid/server/auction/BidResponseCreator.java b/src/main/java/org/prebid/server/auction/BidResponseCreator.java index dd59ceaa6b7..6aeaec15633 100644 --- a/src/main/java/org/prebid/server/auction/BidResponseCreator.java +++ b/src/main/java/org/prebid/server/auction/BidResponseCreator.java @@ -96,6 +96,7 @@ import org.prebid.server.settings.model.AccountAuctionConfig; import org.prebid.server.settings.model.AccountAuctionEventConfig; import org.prebid.server.settings.model.AccountEventsConfig; +import org.prebid.server.settings.model.AccountTargetingConfig; import org.prebid.server.settings.model.VideoStoredDataResult; import org.prebid.server.util.LineItemUtil; import org.prebid.server.util.StreamUtil; @@ -1658,7 +1659,8 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe JsonNode priceGranularity, BidRequest bidRequest, Account account) { - + final int resolvedTruncateAttrChars = resolveTruncateAttrChars(targeting, account); + final String resolveKeyPrefix = resolveKeyPrefix(bidRequest, account, resolvedTruncateAttrChars); return TargetingKeywordsCreator.create( parsePriceGranularity(priceGranularity), BooleanUtils.toBoolean(targeting.getIncludewinners()), @@ -1666,10 +1668,11 @@ private TargetingKeywordsCreator createKeywordsCreator(ExtRequestTargeting targe BooleanUtils.toBoolean(targeting.getAlwaysincludedeals()), BooleanUtils.isTrue(targeting.getIncludeformat()), isApp, - resolveTruncateAttrChars(targeting, account), + resolvedTruncateAttrChars, cacheHost, cachePath, - TargetingKeywordsResolver.create(bidRequest, mapper)); + TargetingKeywordsResolver.create(bidRequest, mapper), + resolveKeyPrefix); } /** @@ -1686,6 +1689,26 @@ private int resolveTruncateAttrChars(ExtRequestTargeting targeting, Account acco truncateAttrChars); } + /** + * Returns targeting key prefix. + * Default prefix for targeting keys used in cases, + * when correspond value is missing in account auction configuration or bid request ext, + * or may compose keys longer than 'settings.targeting.truncate-attr-chars' value. + */ + private static String resolveKeyPrefix(BidRequest bidRequest, Account account, int truncateAttrChars) { + final String prefix = Optional.of(bidRequest) + .map(BidRequest::getExt) + .map(ExtRequest::getPrebid) + .map(ExtRequestPrebid::getTargeting) + .map(ExtRequestTargeting::getPrefix) + .orElse(Optional.ofNullable(account) + .map(Account::getAuction) + .map(AccountAuctionConfig::getTargeting) + .map(AccountTargetingConfig::getPrefix) + .orElse(null)); + return StringUtils.isNotEmpty(prefix) && prefix.length() + 11 < truncateAttrChars ? prefix : "hb"; + } + private static Integer truncateAttrCharsOrNull(Integer value) { return value != null && value >= 0 && value <= 255 ? value : null; } diff --git a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java index 860f545f238..9472f734336 100644 --- a/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java +++ b/src/main/java/org/prebid/server/auction/TargetingKeywordsCreator.java @@ -31,54 +31,54 @@ public class TargetingKeywordsCreator { * Exists to support the Prebid Universal Creative. If it exists, the only legal value is mobile-app. * It will exist only if the incoming bidRequest defiend request.app instead of request.site. */ - private static final String HB_ENV_KEY = "hb_env"; + private static final String ENV_KEY = "_env"; /** - * Used as a value for HB_ENV_KEY. + * Used as a value for ENV_KEY. */ - private static final String HB_ENV_APP_VALUE = "mobile-app"; + private static final String ENV_APP_VALUE = "mobile-app"; /** * Name of the Bidder. For example, "appnexus" or "rubicon". */ - private static final String HB_BIDDER_KEY = "hb_bidder"; + private static final String BIDDER_KEY = "_bidder"; /** * Respects rounded CPM value. */ - private static final String HB_PB_KEY = "hb_pb"; + private static final String PB_KEY = "_pb"; /** * Describes the size in format: [Width]x[Height]. */ - private static final String HB_SIZE_KEY = "hb_size"; + private static final String SIZE_KEY = "_size"; /** * Stores the UUID which can be used to fetch the bid data from prebid cache. * Callers should *never* assume that this exists, since the call to the cache may always fail. */ - private static final String HB_CACHE_ID_KEY = "hb_cache_id"; + private static final String CACHE_ID_KEY = "_cache_id"; /** * Stores the UUID which can be used to fetch the video XML data from prebid cache. * Callers should *never* assume that this exists, since the call to the cache may always fail. */ - private static final String HB_VAST_ID_KEY = "hb_uuid"; + private static final String VAST_ID_KEY = "_uuid"; /** * Stores the deal ID for the given bid. */ - private static final String HB_DEAL_KEY = "hb_deal"; + private static final String DEAL_KEY = "_deal"; /** * Stores protocol, host and port for cache service endpoint. */ - private static final String HB_CACHE_HOST_KEY = "hb_cache_host"; + private static final String CACHE_HOST_KEY = "_cache_host"; /** * Stores http path for cache service endpoint. */ - private static final String HB_CACHE_PATH_KEY = "hb_cache_path"; + private static final String CACHE_PATH_KEY = "_cache_path"; /** * Stores category duration for video bids */ - private static final String HB_CATEGORY_DURATION_KEY = "hb_pb_cat_dur"; + private static final String CATEGORY_DURATION_KEY = "_pb_cat_dur"; /** * Stores bid's format. For example "video" or "banner". */ - private static final String HB_FORMAT_KEY = "hb_format"; + private static final String FORMAT_KEY = "_format"; private static final String DEFAULT_CPM = "0.0"; @@ -92,6 +92,7 @@ public class TargetingKeywordsCreator { private final String cacheHost; private final String cachePath; private final TargetingKeywordsResolver resolver; + private final String keyPrefix; private TargetingKeywordsCreator(PriceGranularity priceGranularity, boolean includeWinners, @@ -102,7 +103,8 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, int truncateAttrChars, String cacheHost, String cachePath, - TargetingKeywordsResolver resolver) { + TargetingKeywordsResolver resolver, + String keyPrefix) { this.priceGranularity = priceGranularity; this.includeWinners = includeWinners; @@ -114,6 +116,7 @@ private TargetingKeywordsCreator(PriceGranularity priceGranularity, this.cacheHost = cacheHost; this.cachePath = cachePath; this.resolver = resolver; + this.keyPrefix = keyPrefix; } /** @@ -128,8 +131,8 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul int truncateAttrChars, String cacheHost, String cachePath, - TargetingKeywordsResolver resolver) { - + TargetingKeywordsResolver resolver, + String keyPrefix) { return new TargetingKeywordsCreator( PriceGranularity.createFromExtPriceGranularity(extPriceGranularity), includeWinners, @@ -140,7 +143,8 @@ public static TargetingKeywordsCreator create(ExtPriceGranularity extPriceGranul truncateAttrChars, cacheHost, cachePath, - resolver); + resolver, + keyPrefix); } /** @@ -199,38 +203,38 @@ private Map makeFor(String bidder, Collections.emptySet()); final String roundedCpm = isPriceGranularityValid() ? CpmRange.fromCpm(price, priceGranularity) : DEFAULT_CPM; - keywordMap.put(HB_PB_KEY, roundedCpm); + keywordMap.put(this.keyPrefix + PB_KEY, roundedCpm); - keywordMap.put(HB_BIDDER_KEY, bidder); + keywordMap.put(this.keyPrefix + BIDDER_KEY, bidder); final String hbSize = sizeFrom(width, height); if (hbSize != null) { - keywordMap.put(HB_SIZE_KEY, hbSize); + keywordMap.put(this.keyPrefix + SIZE_KEY, hbSize); } if (StringUtils.isNotBlank(cacheId)) { - keywordMap.put(HB_CACHE_ID_KEY, cacheId); + keywordMap.put(this.keyPrefix + CACHE_ID_KEY, cacheId); } if (StringUtils.isNotBlank(vastCacheId)) { - keywordMap.put(HB_VAST_ID_KEY, vastCacheId); + keywordMap.put(this.keyPrefix + VAST_ID_KEY, vastCacheId); } if ((StringUtils.isNotBlank(vastCacheId) || StringUtils.isNotBlank(cacheId)) && cacheHost != null && cachePath != null) { - keywordMap.put(HB_CACHE_HOST_KEY, cacheHost); - keywordMap.put(HB_CACHE_PATH_KEY, cachePath); + keywordMap.put(this.keyPrefix + CACHE_HOST_KEY, cacheHost); + keywordMap.put(this.keyPrefix + CACHE_PATH_KEY, cachePath); } if (StringUtils.isNotBlank(format) && includeFormat) { - keywordMap.put(HB_FORMAT_KEY, format); + keywordMap.put(this.keyPrefix + FORMAT_KEY, format); } // get Line Item by dealId if (StringUtils.isNotBlank(dealId)) { - keywordMap.put(HB_DEAL_KEY, dealId); + keywordMap.put(this.keyPrefix + DEAL_KEY, dealId); } if (isApp) { - keywordMap.put(HB_ENV_KEY, HB_ENV_APP_VALUE); + keywordMap.put(this.keyPrefix + ENV_KEY, ENV_APP_VALUE); } if (StringUtils.isNotBlank(categoryDuration)) { - keywordMap.put(HB_CATEGORY_DURATION_KEY, categoryDuration); + keywordMap.put(this.keyPrefix + CATEGORY_DURATION_KEY, categoryDuration); } return keywordMap.asMap(); diff --git a/src/main/java/org/prebid/server/auction/VideoResponseFactory.java b/src/main/java/org/prebid/server/auction/VideoResponseFactory.java index a7f68dbb191..b209eb6ba7b 100644 --- a/src/main/java/org/prebid/server/auction/VideoResponseFactory.java +++ b/src/main/java/org/prebid/server/auction/VideoResponseFactory.java @@ -88,7 +88,7 @@ private List adPodsWithTargetingFrom(List bids) { final List adPods = new ArrayList<>(); for (Bid bid : bids) { final Map targeting = targeting(bid); - if (findByPrefix(targeting, "hb_uuid") == null) { + if (findByPrefix(targeting, "_uuid") == null) { continue; } final String impId = bid.getImpid(); @@ -99,9 +99,9 @@ private List adPodsWithTargetingFrom(List bids) { final Integer podId = Integer.parseInt(podIdString); final ExtResponseVideoTargeting videoTargeting = ExtResponseVideoTargeting.of( - findByPrefix(targeting, "hb_pb"), - findByPrefix(targeting, "hb_pb_cat_dur"), - findByPrefix(targeting, "hb_uuid")); + findByPrefix(targeting, "_pb"), + findByPrefix(targeting, "_pb_cat_dur"), + findByPrefix(targeting, "_uuid")); ExtAdPod adPod = adPods.stream() .filter(extAdPod -> extAdPod.getPodid().equals(podId)) @@ -136,7 +136,7 @@ private Map targeting(Bid bid) { private static String findByPrefix(Map keyToValue, String prefix) { return keyToValue.entrySet().stream() - .filter(keyAndValue -> keyAndValue.getKey().startsWith(prefix)) + .filter(keyAndValue -> keyAndValue.getKey().contains(prefix)) .map(Map.Entry::getValue) .findFirst() .orElse(null); diff --git a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java index afcfe29af08..b4143aaa0c2 100644 --- a/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java +++ b/src/main/java/org/prebid/server/handler/openrtb2/AmpHandler.java @@ -186,7 +186,7 @@ private Map targetingFrom(Bid bid, String bidder) { // go in the AMP response final Map targeting = extBidPrebid != null ? extBidPrebid.getTargeting() : null; if (targeting != null && targeting.keySet().stream() - .anyMatch(key -> key != null && key.startsWith("hb_cache_id"))) { + .anyMatch(key -> key != null && key.contains("_cache_id"))) { return enrichWithCustomTargeting(targeting, bidExt, bidder); } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java index 56d4a40fc45..50fb4b27683 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestTargeting.java @@ -67,4 +67,9 @@ public class ExtRequestTargeting { @JsonAlias("alwaysIncludeDeals") Boolean alwaysincludedeals; + + /** + * Defines the contract for bidrequest.ext.prebid.targeting.prefix + */ + String prefix; } diff --git a/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java b/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java index 7bcb6992186..a47dfa0a614 100644 --- a/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java +++ b/src/main/java/org/prebid/server/settings/model/AccountTargetingConfig.java @@ -22,4 +22,7 @@ public class AccountTargetingConfig { @JsonProperty("alwaysincludedeals") Boolean alwaysIncludeDeals; + + @JsonProperty("prefix") + String prefix; } diff --git a/src/main/java/org/prebid/server/validation/RequestValidator.java b/src/main/java/org/prebid/server/validation/RequestValidator.java index b426ed0bf15..71750ca3f72 100644 --- a/src/main/java/org/prebid/server/validation/RequestValidator.java +++ b/src/main/java/org/prebid/server/validation/RequestValidator.java @@ -410,6 +410,22 @@ private void validateTargeting(ExtRequestTargeting extRequestTargeting) throws V throw new ValidationException("ext.prebid.targeting: At least one of includewinners or includebidderkeys" + " must be enabled to enable targeting support"); } + + validateTargetingPrefix(extRequestTargeting); + } + + private void validateTargetingPrefix(ExtRequestTargeting extRequestTargeting) throws ValidationException { + final Integer truncateattrchars = extRequestTargeting.getTruncateattrchars(); + final int prefixLength = extRequestTargeting.getPrefix() != null + ? extRequestTargeting.getPrefix().length() + : 0; + final boolean prefixLengthInvalid = truncateattrchars != null + && prefixLength > 0 + && prefixLength + 11 > truncateattrchars; // 11 - length of the longest targeting keyword without prefix + if (prefixLengthInvalid) { + throw new ValidationException("ext.prebid.targeting: decrease prefix length or increase truncateattrchars" + + " by " + (prefixLength + 11 - truncateattrchars) + " characters"); + } } /** diff --git a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java index eb6145ca096..7b0a04e4431 100644 --- a/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java +++ b/src/test/java/org/prebid/server/auction/TargetingKeywordsCreatorTest.java @@ -24,6 +24,7 @@ public class TargetingKeywordsCreatorTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + private final String defaultKeyPrefix = "hb"; @Test public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { @@ -44,7 +45,8 @@ public void shouldReturnTargetingKeywordsForOrdinaryBidOpenrtb() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", false, null, null, null, null); // then @@ -74,7 +76,8 @@ public void shouldReturnTargetingKeywordsWithEntireKeysOpenrtb() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "veryververyverylongbidder1", false, null, null, null, null); // then @@ -108,7 +111,8 @@ public void shouldReturnTargetingKeywordsForWinningBidOpenrtb() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, "cacheId1", "banner", "videoCacheId1", "categoryDuration"); // then @@ -150,7 +154,8 @@ public void shouldIncludeFormatOpenrtb() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "", true, null, "banner", null, null); // then @@ -175,7 +180,8 @@ public void shouldNotIncludeCacheIdAndDealIdAndSizeOpenrtb() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder", true, null, null, null, null); // then @@ -201,7 +207,8 @@ public void shouldReturnEnvKeyForAppRequestOpenrtb() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder", true, null, null, null, null); // then @@ -228,7 +235,8 @@ public void shouldNotIncludeWinningBidTargetingIfIncludeWinnersFlagIsFalse() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, null, null, null, null); // then @@ -253,7 +261,8 @@ public void shouldIncludeWinningBidTargetingIfIncludeWinnersFlagIsTrue() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, null, null, null, null); // then @@ -278,7 +287,8 @@ public void shouldNotIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsFalse() 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, null, null, null, null); // then @@ -303,7 +313,8 @@ public void shouldIncludeBidderKeysTargetingIfIncludeBidderKeysFlagIsTrue() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, null, null, null, null); // then @@ -328,7 +339,8 @@ public void shouldTruncateTargetingBidderKeywordsIfTruncateAttrCharsIsDefined() 20, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null); // then @@ -354,7 +366,8 @@ public void shouldTruncateTargetingWithoutBidderSuffixKeywordsIfTruncateAttrChar 7, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder", true, null, null, null, null); // then @@ -380,7 +393,8 @@ public void shouldTruncateTargetingAndDropDuplicatedWhenTruncateIsTooShort() { 6, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder", true, null, null, null, null); // then @@ -407,7 +421,8 @@ public void shouldNotTruncateTargetingKeywordsIfTruncateAttrCharsIsNotDefined() 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "someVeryLongBidderName", true, null, null, null, null); // then @@ -439,7 +454,8 @@ public void shouldTruncateKeysFromResolver() { 20, null, null, - resolver) + resolver, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, null, null, null, null); // then @@ -470,7 +486,8 @@ public void shouldIncludeKeywordsFromResolver() { 0, null, null, - resolver) + resolver, + defaultKeyPrefix) .makeFor(bid, "bidder1", true, null, null, null, null); // then @@ -495,7 +512,8 @@ public void shouldIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsTrue() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", false, null, null, null, null); // then @@ -520,7 +538,8 @@ public void shouldNotIncludeDealBidTargetingIfAlwaysIncludeDealsFlagIsFalse() { 0, null, null, - null) + null, + defaultKeyPrefix) .makeFor(bid, "bidder1", false, null, null, null, null); // then diff --git a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java index ccf01a13877..3ff235d1188 100644 --- a/src/test/java/org/prebid/server/validation/RequestValidatorTest.java +++ b/src/test/java/org/prebid/server/validation/RequestValidatorTest.java @@ -1879,6 +1879,34 @@ public void validateShouldReturnValidationMessageForInvalidTargeting() { + " must be enabled to enable targeting support"); } + @Test + public void validateShouldReturnValidationMessageForInvalidTargetingPrefix() { + // given + final ExtPriceGranularity priceGranularity = ExtPriceGranularity.of(1, singletonList( + ExtGranularityRange.of(BigDecimal.valueOf(5), BigDecimal.valueOf(0.01)))); + final String prefix = "1234567890"; + final int truncateattrchars = 10; + final BidRequest bidRequest = validBidRequestBuilder() + .ext(ExtRequest.of(ExtRequestPrebid.builder() + .targeting(ExtRequestTargeting.builder() + .pricegranularity(mapper.valueToTree(priceGranularity)) + .includebidderkeys(true) + .includewinners(true) + .truncateattrchars(truncateattrchars) + .prefix(prefix) + .build()) + .build())) + .build(); + + // when + final ValidationResult result = target.validate(bidRequest, null); + + // then + assertThat(result.getErrors()).hasSize(1) + .containsOnly("ext.prebid.targeting: decrease prefix length or increase truncateattrchars" + + " by " + (prefix.length() + 11 - truncateattrchars) + " characters"); + } + @Test public void validateShouldReturnValidationMessageWhenRangesContainsMissedMaxValue() { final ExtPriceGranularity priceGranuality = ExtPriceGranularity.of(2,